Einleitung

Im ersten Teil haben wir die wichtigsten Konzepte kennengelernt. Es wurde ein wenig theoretischer und wir haben viel über R kennengelernt. Ich möchte Teil 1 ein wenig mit der Grundgrammatik vergleichen.
In Teil 2 geht es um Data Science. Hier lernen wir dia Anwendung kennen, mehr Vokabeln, mehr Spaß?
Wir werden sehen.

Was lernen wir?

In einem typischen Data Science Projekt haben wir folgende Schritte:

Zuerst müssen wir die Daten importieren. Aus einer Datei oder dem Netz oder einer Datenbank für gewöhnlich, so dass wir eine schöne, übersichtliche Tabelle haben. Im besten Fall versteht sich.

Danach muss aufgeräumt werden. Spalten mit Variablen und Zeilen mit Beobachtungen liegen uns vor, so dass wir uns im weiteren nicht mehr um nervige Aufräumarbeit kümmern müssen.

Haben wir aufgeräumt, so werden die Daten transformiert. Relevante Variablen werden zusammengefasst, neue Variablen entstehen aus alten durch Funktionen, summary statistics werden berechnet. Aufräumen und Transformation nennen wir zusammen: Wrangling.

Danach wird es spannend: Visualisation und Modelle erstellen steht im Vordergrund. Eine Grafik sagt mehr als 1000 Worte. Richtig romantisch, oder?

Kommunikation folgt dann. Kommuniziere deine Ergebnisse zu Anderen. Dies wird oft vernachlässigt, ist aber nicht immer so leicht wie man denkt.

Umfasst werden all diese Werkzeuge von der Programmierung. Um ein erfolgreicher Data Scientist zu sein, musst du nicht auch ein Experte in Sachen Programmierung sein. Aber ein besserer Programmierer zu sein hilft, da es dir erlaubt viele Aufgaben zu automatisieren und erheblich zu beschleunigen.

Voraussetzungen

Es hilft natürlich sich mit den wichtigsten Konzepten von Teil 1 auseinandergesetzt zu haben. Neben R und RStudio brauchen wir noch das tidyverse Paket und viele weitere.

tidyverse

Ein Paket ist eine Kollektion von Funktionen, Daten und Dokumentationen von R. Funktionen in R zu nutzen ist das Erfolgsgeheimis von R.
Die meisten Pakete, die wir hier kennenlernen sind Teil vom tidyverse Paket. Mit einer Zeile Code kannst du tidyverse installieren.

install.packages("tidyverse")

Anschließend musst du natürlich noch das Paket laden:

library(tidyverse)
## Warning: Paket 'tidyverse' wurde unter R Version 4.2.3 erstellt
## Warning: Paket 'ggplot2' wurde unter R Version 4.2.3 erstellt
## Warning: Paket 'tibble' wurde unter R Version 4.2.3 erstellt
## Warning: Paket 'dplyr' wurde unter R Version 4.2.3 erstellt
## Warning: Paket 'stringr' wurde unter R Version 4.2.3 erstellt
## Warning: Paket 'lubridate' wurde unter R Version 4.2.3 erstellt
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.1     ✔ readr     2.1.4
## ✔ forcats   1.0.0     ✔ stringr   1.5.0
## ✔ ggplot2   3.4.1     ✔ tibble    3.2.1
## ✔ lubridate 1.9.2     ✔ tidyr     1.3.0
## ✔ purrr     1.0.1     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

So gehen wir immer vor, wenn Pakete benötigt werden. Sie erleichtern uns die Arbeit. Acht Pakete werden hier auf einmal geladen: ggplot2, tibble, tidyr, readr, purrr, dplyr, stringr und forcats. Bei fast jeder Analyse brauchst du sie.

Weitere Pakete

In diesem Teil brauchen wir wahrscheinlich noch drei weitere Pakete:

install.packages(c("nycflights13", "gapminder", "Lahman"))
library(nycflights13)
## Warning: Paket 'nycflights13' wurde unter R Version 4.2.3 erstellt
library(gapminder)
## Warning: Paket 'gapminder' wurde unter R Version 4.2.3 erstellt
library(Lahman)
## Warning: Paket 'Lahman' wurde unter R Version 4.2.3 erstellt

Sie liefern uns Daten.

Daten visualisieren

In diesem Kapitel fokusieren wir uns auf ggplot2. Das Paket haben wir schon über library(tidyverse) geladen. Falls nicht, holen wir dies schnell nach.

library(tidyverse)

Ein Paket musst du nur einmal installieren, es aber bei jeder neuen Session wieder laden. Hinzo kommen jetzt Daten von Pinguinen.

install.packages("palmerpenguins")
library(palmerpenguins)
## Warning: Paket 'palmerpenguins' wurde unter R Version 4.2.3 erstellt

Fragen

Gibt es einen Zusammenhang zwischen der Flossenlänge eines Pinguins und seinem Gewicht? Wie sieht dieser Zusammenhang aus? Hängt er von der Spezie der Pinguine ab? Und vielleicht sogar von der Herkunft der Pinguine?

penguins data frame

Dieser Datensatz enthält 344 Zeilen und 7 Spalten.

Einen alternativen Blick kannst du mit glimpse() auf die Daten werfen. Oder mit View(penguins).

glimpse(penguins)
## Rows: 344
## Columns: 8
## $ species           <fct> Adelie, Adelie, Adelie, Adelie, Adelie, Adelie, Adel…
## $ island            <fct> Torgersen, Torgersen, Torgersen, Torgersen, Torgerse…
## $ bill_length_mm    <dbl> 39.1, 39.5, 40.3, NA, 36.7, 39.3, 38.9, 39.2, 34.1, …
## $ bill_depth_mm     <dbl> 18.7, 17.4, 18.0, NA, 19.3, 20.6, 17.8, 19.6, 18.1, …
## $ flipper_length_mm <int> 181, 186, 195, NA, 193, 190, 181, 195, 193, 190, 186…
## $ body_mass_g       <int> 3750, 3800, 3250, NA, 3450, 3650, 3625, 4675, 3475, …
## $ sex               <fct> male, female, female, NA, female, male, female, male…
## $ year              <int> 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007, 2007…

species, flipper_length_mm und body_mass_g gehören zu den Variablen. Mehr Infos unter ?penguins.

Wir wollen jetzt das Gewicht in Abhängigkeit von der Flossenlänge darstellen. Für alle drei Spezien getrennt.

ggplot erstellen

Das erste Argument von ggplot() ist der Datensatz.

ggplot(data = penguins)

Es entsteht ein leerer Graph. Wir brauchen natürlich noch die Variablen.
Das mapping Argument der ggplot() Funktion definiert wie Variablen abgebildet werden, um sie zu visualisieren. Es kommt immer mit der aes() Funktion und den x und y Argumenten von aes() daher.
Bei uns soll die Flossenlänge auf der x-Achse sein und der Body Maß auf der y-Achse.

ggplot(data = penguins,
       mapping = aes(x = flipper_length_mm))

ggplot(data = penguins,
       mapping = aes(x = flipper_length_mm, y = body_mass_g))

Unsere leere Leinwand wurde jetzt ein wenig gefüllt. Aber wo sind die Pinguine? Noch nicht da, weil wir noch nicht gesagt haben wie die Beobachtungen auf den Plot angewendet werden sollen. Um das zu machen müssen wir ein geom definieren. Dieses Objekt benutzt einen Plot um Daten zu repräsentieren. Oftmals wird der Typ des Plots an geom_ angehängt wie z.B. geom_bar, geom_line, geom_boxplot oder geom_point.
Mithilfe von geom_point werden Punkte zum Plot hinzugefügt, so dass ein Scatterplot entsteht. Insgesamt gibt es sehr viele geom Funktionen.

ggplot(data = penguins,
       mapping = aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point()
## Warning: Removed 2 rows containing missing values (`geom_point()`).

Wir haben einen einigermaßen linearen, positiven Zusammenhang. Genauso wie erwartet zwischen Flossenlänge und Gewicht. Nur die verschiedenen Spezien fehlen noch.
Eine Fehlermeldung wurde aber auch noch ausgegeben. Hier fehlen 2 Werte, siehe Tabelle oder hier (der Code ist neu für uns. Keine Sorge, Erklärung kommt noch).

penguins |>
  select(species, flipper_length_mm, body_mass_g) |>
  filter(is.na(body_mass_g) | is.na(flipper_length_mm))
## # A tibble: 2 × 3
##   species flipper_length_mm body_mass_g
##   <fct>               <int>       <int>
## 1 Adelie                 NA          NA
## 2 Gentoo                 NA          NA

Missing Values haben ihre Daseinsberechtigung. Kein Witz.

Hinzufügen von aesthetics und layers

Den Zusammenhang zwischen zwei Variablen darzustellen ist schön und gut. Doch oftmals fragt man sich, ob es nicht noch weitere Variablen gibt, die den Zusammenhang erklären oder ändern.
In unserem Beispiel nehmen wir noch die Spezie hinzu. Aber wo genau? In das aesthetic mapping, in die aes() Funktion.

ggplot(data = penguins,
       mapping = aes(x = flipper_length_mm, y = body_mass_g,
                     color = species)) +
  geom_point()
## Warning: Removed 2 rows containing missing values (`geom_point()`).

Da eine weitere Variable in das aes() aufgenommen wurde, hat ggplot2 dieser automatisch einen einzigartigen Wert zugewiesen, hier eine eine Farbe jedem Level der Variable. Das nennt sich scaling. Auch eine Legende wurde automatisch hinzugefügt. Weitere Schichten (layers) sind möglich.
Zum Beispiel eine glatte Kurve, die den Zusammenhang anschaulich wiedergibt. Ein neues geom wird hinzugefügt: geom_smooth().

ggplot(data = penguins,
       mapping = aes(x = flipper_length_mm, y = body_mass_g,
                     color = species)) +
  geom_point() +
  geom_smooth()
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
## Warning: Removed 2 rows containing non-finite values (`stat_smooth()`).
## Warning: Removed 2 rows containing missing values (`geom_point()`).

Wir wollen aber nur eine Kurve für alle Spezien zusammen. Wie geht das denn?

Wir haben aes mappings in ggplot() definiert, im globalen Level. Jetzt werden sie weiter vererbt von jeder folgenden geom Schicht des Plots. Wir können aber jeder geom Funktion in ggplot2 ein lokales mapping verpassen. Wollen wir farbige Punkte für die verschiedenen Spezien, aber eine Kurve für alle, so können wir nur für die Punkte geom_point() eine Unterscheidung vornehmen, durch color = species.

ggplot(data = penguins,
       mapping = aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point(mapping = aes(color = species)) +
  geom_smooth()
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
## Warning: Removed 2 rows containing non-finite values (`stat_smooth()`).
## Warning: Removed 2 rows containing missing values (`geom_point()`).

So gefällt uns das doch schon viel besser. Uns reichen verschiedene Farben aber nicht aus. Wir wollen auch verschiedene Symbole. Es gibt auch Farbenblinde, auch an die denken wir jetzt einmal.

Zusätzlich können wir auch species zum shape aes hinzufügen.

ggplot(data = penguins,
       mapping = aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point(mapping = aes(color = species, shape = species)) +
  geom_smooth()
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
## Warning: Removed 2 rows containing non-finite values (`stat_smooth()`).
## Warning: Removed 2 rows containing missing values (`geom_point()`).

Als weitere Schicht können wir noch die labs() Funktion hinzufügen. Alles (Labels) sieht dann noch schöner aus.

ggplot(penguins, 
       aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point(aes(color = species, shape = species)) +
  geom_smooth() +
  labs(
    title = "Body mass and flipper length",
    subtitle = "Dimensions for Adelie, Chinstrap, and Gentoo Penguins",
    x = "Flipper length (mm)", 
    y = "Body mass (g)",
    color = "Species", 
    shape = "Species"
  )
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
## Warning: Removed 2 rows containing non-finite values (`stat_smooth()`).
## Warning: Removed 2 rows containing missing values (`geom_point()`).

Die Ausdrücke data = penguins und mapping = aes() können wir auch verkürzen zu penguins und aes().

ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) + 
  geom_point()

Später lernen wir die Pipe kennen, dann können wir weiter vereinfachen.

penguins |> 
  ggplot(aes(x = flipper_length_mm, y = body_mass_g)) + 
  geom_point()

Verteilungen visualisieren

Die Visualisierung hängt natürlich vom Datentyp bzw. dem Skalenniveau ab: kategorial oder numerisch.

kategoriale Variable

Für kategoriale Variablen bietet sich ein bar chart an, ein Säulendiagramm. Es zählt die Häufigkeit der Variable x.

ggplot(penguins, aes(x = species)) +
  geom_bar()

Die Reihenfolge wird oft durch das Alphabet vorgenommen, kann aber nach Häufigkeit vorgenommen werden. Dafür müssen wir aber die Variable zu einem Faktor transformieren. Danach können wir neu sortieren.

ggplot(penguins, aes(x = fct_infreq(species))) +
  geom_bar()

Mehr dazu später.

Numerische Variable

Um die Verteilung einer stetigen Variable zu visualisieren, benutze ein Histogramm oder einen density (Dichte) plot.

ggplot(penguins, aes(x = body_mass_g)) +
  geom_histogram(binwidth = 200)
## Warning: Removed 2 rows containing non-finite values (`stat_bin()`).

ggplot(penguins, aes(x = body_mass_g)) +
  geom_density()
## Warning: Removed 2 rows containing non-finite values (`stat_density()`).

Wir sehen, dass 39 Pinguine ein Gewicht zwischen 3500 g und 3700 g haben (Klassengrenzen, Klassenbreite = 200).

penguins |>
  count(cut_width(body_mass_g, 200))
## # A tibble: 19 × 2
##    `cut_width(body_mass_g, 200)`     n
##    <fct>                         <int>
##  1 [2.7e+03,2.9e+03]                 7
##  2 (2.9e+03,3.1e+03]                10
##  3 (3.1e+03,3.3e+03]                23
##  4 (3.3e+03,3.5e+03]                38
##  5 (3.5e+03,3.7e+03]                39
##  6 (3.7e+03,3.9e+03]                37
##  7 (3.9e+03,4.1e+03]                28
##  8 (4.1e+03,4.3e+03]                25
##  9 (4.3e+03,4.5e+03]                20
## 10 (4.5e+03,4.7e+03]                22
## 11 (4.7e+03,4.9e+03]                21
## 12 (4.9e+03,5.1e+03]                17
## 13 (5.1e+03,5.3e+03]                13
## 14 (5.3e+03,5.5e+03]                14
## 15 (5.5e+03,5.7e+03]                16
## 16 (5.7e+03,5.9e+03]                 6
## 17 (5.9e+03,6.1e+03]                 5
## 18 (6.1e+03,6.3e+03]                 1
## 19 <NA>                              2

Probiere verschiedene Bandbreiten aus, um das anschaulichste Histogramm zu erhalten.

ggplot(penguins, aes(x = body_mass_g)) +
  geom_histogram(binwidth = 20)
## Warning: Removed 2 rows containing non-finite values (`stat_bin()`).

ggplot(penguins, aes(x = body_mass_g)) +
  geom_histogram(binwidth = 200)
## Warning: Removed 2 rows containing non-finite values (`stat_bin()`).

ggplot(penguins, aes(x = body_mass_g)) +
  geom_histogram(binwidth = 2000)
## Warning: Removed 2 rows containing non-finite values (`stat_bin()`).

Zusammenhänge visualisieren

Dafür brauchen wir natürlich mindestens zwei Variablen.

Eine numerische und eine kategoriale Variable

Boxplots natürlich.

ggplot(penguins, aes(x = species, y = body_mass_g)) +
  geom_boxplot()
## Warning: Removed 2 rows containing non-finite values (`stat_boxplot()`).

Alternativ bieten sich Häufigkeits-Polygonzüge an, mit geom_freqpoly(). Statt konstante Höhen werden hier Linien benutzt.

ggplot(penguins, aes(x = body_mass_g, color = species)) +
  geom_freqpoly(binwidth = 200, linewidth = 0.75)
## Warning: Removed 2 rows containing non-finite values (`stat_bin()`).

Wir können die Dicke der Linien mit linewidth anpassen. Wir können density plots anschaulich überlappen, sie transparent machen, Farben benutzen und sie befüllen. Rund werden sie auch noch. Der “Transparenzwert” alpha liegt zwischen 0 (komplett transparent) und 1.

ggplot(penguins, aes(x = body_mass_g, color = species, fill = species)) +
  geom_density(alpha = 0.5)
## Warning: Removed 2 rows containing non-finite values (`stat_density()`).

Zwei kategoriale Variablen

Die erste Variable wird unter x aes auf der x-Achse abgetragen und die zweite Variable wird dem fill aes zugeordnet. Farblich wird dann jeder Balken noch einmal unterteilt (bzgl. der Variable).

Die Plots sind dann selbsterklärend.

ggplot(penguins, aes(x = island, fill = species)) +
  geom_bar()

ggplot(penguins, aes(x = island, fill = species)) +
  geom_bar(position = "fill")

Zwei numerische Variablen

Ein Scatterplot ist die weitverbreiteste Darstellungsart von zwei numerischen Variablen.

ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point()
## Warning: Removed 2 rows containing missing values (`geom_point()`).

Drei oder mehr Variablen

Eine Möglichkeit ist es, die Variablen einem aes zuzuordnen. Neben x-Achse und y-Achse haben wir so insgesamt 4 Variablen: species und island noch dazu.

ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point(aes(color = species, shape = island))
## Warning: Removed 2 rows containing missing values (`geom_point()`).

Zu viele aes machen einen Plot aber unübersichtlich. Eine andere, nützliche Möglichkeit bei kategorialen Variablen sind Subplots, facets genannt.
Benutze dafür facet_wrap() Das erste Argument ist eine Tilde, das zweite der Variablenname. Diese Variable sollte natürlich kategorial sein.

ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point(aes(color = species, shape = species)) +
  facet_wrap(~island)
## Warning: Removed 2 rows containing missing values (`geom_point()`).

Plots speichern

ggsave() speichert.

ggplot(penguins, aes(x = flipper_length_mm, y = body_mass_g)) +
  geom_point()
## Warning: Removed 2 rows containing missing values (`geom_point()`).

#> Warning: Removed 2 rows containing missing values (`geom_point()`).
ggsave(filename = "my-plot.png")
## Saving 7 x 5 in image
## Warning: Removed 2 rows containing missing values (`geom_point()`).
#> Saving 6 x 4 in image
#> Warning: Removed 2 rows containing missing values (`geom_point()`).

Dieser Befehl speichert den Plot in der working directory. Notfalls sucht die Datei auf eurem Computer. Mehr dazu unter ?sec-workflow-scripts.

Gängige Probleme

Denke daran deine Klammern wieder zu schließen. Wenn du ein + siehst. R wartet darauf, dass du deinen Befehl zu Ende schreibst. Das + steht am Ende der Zeile, nicht am Anfang.
Scheue dich nicht google.com zu benutzen, hier findest du wirklich alles. Oder Chat GPT.

Daten transformieren

Einleitung

Visualisierung ist schön und gut. Aber es ist doch sehr selten, dass du deine Daten genau in der gewünschten Form bekommst. Oft musst du neue Variablen erstellen, andere zusammenfassen, oder umbenennen, Werte in ihnen neu sortieren. All das lernen wir in diesem Kapitel. Dafür brauchen wir das dplyr Paket und einen neuen Datensatz.

Voraussetzungen

library(nycflights13)
library(tidyverse)

nycflights13

Der Datensatz enthält 336 776 Flüge, die 2013 von NYC aus geflogen wurden.

flights
## # A tibble: 336,776 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 336,766 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Dieser Datensatz sieht jetzt optisch ein wenig anders aus, als wir es (von früher) gewohnt sind. Weil er ein tibble ist, ein spezieller Typ von Data Frame. Es werden hier nur die ersten Zeilen angezeigt. Auch nicht alle Spalten, gersde so viele wie auf den Bildschirm passen oder ins Fenster. Der Datentyp ist für große Datensätze gemacht. View(flights) bietet eine interaktive Ansicht, ähnlich wie in Excel. Mit print(flights, width = Inf) kannst du alle Spalten anzeigen lassen. Oder mit glimpse().

glimpse(flights)
## Rows: 336,776
## Columns: 19
## $ year           <int> 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2…
## $ month          <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
## $ day            <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
## $ dep_time       <int> 517, 533, 542, 544, 554, 554, 555, 557, 557, 558, 558, …
## $ sched_dep_time <int> 515, 529, 540, 545, 600, 558, 600, 600, 600, 600, 600, …
## $ dep_delay      <dbl> 2, 4, 2, -1, -6, -4, -5, -3, -3, -2, -2, -2, -2, -2, -1…
## $ arr_time       <int> 830, 850, 923, 1004, 812, 740, 913, 709, 838, 753, 849,…
## $ sched_arr_time <int> 819, 830, 850, 1022, 837, 728, 854, 723, 846, 745, 851,…
## $ arr_delay      <dbl> 11, 20, 33, -18, -25, 12, 19, -14, -8, 8, -2, -3, 7, -1…
## $ carrier        <chr> "UA", "UA", "AA", "B6", "DL", "UA", "B6", "EV", "B6", "…
## $ flight         <int> 1545, 1714, 1141, 725, 461, 1696, 507, 5708, 79, 301, 4…
## $ tailnum        <chr> "N14228", "N24211", "N619AA", "N804JB", "N668DN", "N394…
## $ origin         <chr> "EWR", "LGA", "JFK", "JFK", "LGA", "EWR", "EWR", "LGA",…
## $ dest           <chr> "IAH", "IAH", "MIA", "BQN", "ATL", "ORD", "FLL", "IAD",…
## $ air_time       <dbl> 227, 227, 160, 183, 116, 150, 158, 53, 140, 138, 149, 1…
## $ distance       <dbl> 1400, 1416, 1089, 1576, 762, 719, 1065, 229, 944, 733, …
## $ hour           <dbl> 5, 5, 5, 5, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 6, 6…
## $ minute         <dbl> 15, 29, 40, 45, 0, 58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0…
## $ time_hour      <dttm> 2013-01-01 05:00:00, 2013-01-01 05:00:00, 2013-01-01 0…

Hier ist auch der Typ jeder Variable angegeben: <int> für integer, <dbl> für double, <chr> für character und <dttm> für date-time. Wichtig sind sie, da die Operationen, die du auf sie ausführst, vom Datentyp abhängen.

dplyr basics

Die wichtigsten dplyr verbs lernen wir hier kennen. Sie erlauben uns die meiste Arbeit der Datenmanipulation zu verrichten. Was haben sie alle gemeinsam?

  1. Das erste Argument ist ein Data Frame.

  2. Die nachfolgenden Argumente beschreiben was mit dem Data Frame zu machen ist. Sie benutzen die Variablennamen hierbei (ohne Anführungszeichen).

  3. Das Ergebnis ist ein neuer Data Frame.

Da das erste Argument immer ein Data Frame ist, und auch der Output, arbeiten dplyr verbs gut mit der pipe, |>:

x |> f(y) ist äquivalent zu f(x, y)

und

x |> f(y) |> g(z) ist äquivalent zu g(f(x, y), z).

Ausgesprochen wir die pipe als “dann” oder “then”.

flights |>
  filter(dest == "IAH") |> 
  group_by(year, month, day) |> 
  summarize(
    arr_delay = mean(arr_delay, na.rm = TRUE)
  )
## `summarise()` has grouped output by 'year', 'month'. You can override using the
## `.groups` argument.
## # A tibble: 365 × 4
## # Groups:   year, month [12]
##     year month   day arr_delay
##    <int> <int> <int>     <dbl>
##  1  2013     1     1     17.8 
##  2  2013     1     2      7   
##  3  2013     1     3     18.3 
##  4  2013     1     4     -3.2 
##  5  2013     1     5     20.2 
##  6  2013     1     6      9.28
##  7  2013     1     7     -7.74
##  8  2013     1     8      7.79
##  9  2013     1     9     18.1 
## 10  2013     1    10      6.68
## # ℹ 355 more rows
fd  <- data.frame(A = c(2009, 2009, 2009, 2010, 2010), B = c(3,4,5,6,8))
fd|>
  group_by(A)|>
  summarise(B = mean(B))
## # A tibble: 2 × 2
##       A     B
##   <dbl> <dbl>
## 1  2009     4
## 2  2010     7

Der Code startet mit dem flights Datensatz, dann wird gefiltert, dann gruppiert, dann zusammengefasst.
dplyr’s verbs sind in 4 Gruppen organisiert: rows, columns, groups, tables.

Rows

Die wichtigsten Verben, die auf Reihen angewendet werden, sind filter() und arrange(). Beide Funktionen affektieren nur die Zeilen. Die Spalten bleiben unberührt. distinct findet Zeilen mit einzigartigen Values. Es kann auch die Spalten verändern.

filter()

Wir behalten die Reihen bei, die bestimmte Werte der Spalten haben. Das erste Argument ist ein Data Frame, danach folgen die Bedingugnen, die erfüllt sein müssen. Alle Flüge mit mehr als 120 Minuten Verspätung:

flights |> 
  filter(arr_delay > 120)
## # A tibble: 10,034 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      811            630       101     1047            830
##  2  2013     1     1      848           1835       853     1001           1950
##  3  2013     1     1      957            733       144     1056            853
##  4  2013     1     1     1114            900       134     1447           1222
##  5  2013     1     1     1505           1310       115     1638           1431
##  6  2013     1     1     1525           1340       105     1831           1626
##  7  2013     1     1     1549           1445        64     1912           1656
##  8  2013     1     1     1558           1359       119     1718           1515
##  9  2013     1     1     1732           1630        62     2028           1825
## 10  2013     1     1     1803           1620       103     2008           1750
## # ℹ 10,024 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Natürlich kannst du auch die bekannten Operatoren:

  • >
  • >=
  • <
  • <=
  • ==
  • !=

benutzen, und auch & (und) oder | (oder).

# Flights that departed on January 1
flights |> 
  filter(month == 1 & day == 1)
## # A tibble: 842 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 832 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Kombinationen sind also natürlich möglich.

# Flights that departed in January or February
flights |> 
  filter(month == 1 | month == 2)
## # A tibble: 51,955 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 51,945 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Bekannt ist auch der %in% Operator, der die Kombination | und == ersetzt. Zeilen werden behalten, bei denen die Variable einem der Werte auf der rechten Seite entspricht.

# A shorter way to select flights that departed in January or February
flights |> 
  filter(month %in% c(1, 2))
## # A tibble: 51,955 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 51,945 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

dplyr Funktionen modifizieren niemals ihren input data frame, flights bleibt also erhalten. Um das Ergebnis zu speichern, musst du also den assign Operator <- bemühen.

jan1 <- flights |> 
  filter(month == 1 & day == 1)
jan1
## # A tibble: 842 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 832 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

arrange()

arrange() ändert die Reihenfolge der Reihen, basierend auf den Werten der Spalten. Als Argumente kommen nach dem Data Frame die Spalten, die sortiert werden sollen. Von klein nach groß. Erst Jahr, dann Monat, usw.

flights |> 
  arrange(year, month, day, dep_time)
## # A tibble: 336,776 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 336,766 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Du kannst natürlich auch andersrum sortieren mit desc().

flights |> 
  arrange(desc(dep_delay))
## # A tibble: 336,776 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     9      641            900      1301     1242           1530
##  2  2013     6    15     1432           1935      1137     1607           2120
##  3  2013     1    10     1121           1635      1126     1239           1810
##  4  2013     9    20     1139           1845      1014     1457           2210
##  5  2013     7    22      845           1600      1005     1044           1815
##  6  2013     4    10     1100           1900       960     1342           2211
##  7  2013     3    17     2321            810       911      135           1020
##  8  2013     6    27      959           1900       899     1236           2226
##  9  2013     7    22     2257            759       898      121           1026
## 10  2013    12     5      756           1700       896     1058           2020
## # ℹ 336,766 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

arrange() und filter() können natürlich kombiniert werden.

flights |> 
  filter(dep_delay <= 10 & dep_delay >= -10) |> 
  arrange(desc(arr_delay))
## # A tibble: 239,109 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013    11     1      658            700        -2     1329           1015
##  2  2013     4    18      558            600        -2     1149            850
##  3  2013     7     7     1659           1700        -1     2050           1823
##  4  2013     7    22     1606           1615        -9     2056           1831
##  5  2013     9    19      648            641         7     1035            810
##  6  2013     4    18      655            700        -5     1213            950
##  7  2013     6    30     1423           1425        -2     1816           1554
##  8  2013     6    24     1523           1520         3     1931           1710
##  9  2013     3    18     1844           1847        -3       39           2219
## 10  2013     7     1      905            905         0     1443           1223
## # ℹ 239,099 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

distinct()

Einzigartige Zeilen werden mit distinct() gefunden. Meistens wollen wir jedoch nur eine einzigartige Kombination von ein paar Variablen. Dann brauchen wir diese natürlich als Argument für distinct.

# This would remove any duplicate rows if there were any
flights |> 
  distinct()
## # A tibble: 336,776 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 336,766 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Oder natürlich.

# This finds all unique origin and destination pairs.
flights |> 
  distinct(origin, dest)
## # A tibble: 224 × 2
##    origin dest 
##    <chr>  <chr>
##  1 EWR    IAH  
##  2 LGA    IAH  
##  3 JFK    MIA  
##  4 JFK    BQN  
##  5 LGA    ATL  
##  6 EWR    ORD  
##  7 EWR    FLL  
##  8 LGA    IAD  
##  9 JFK    MCO  
## 10 LGA    ORD  
## # ℹ 214 more rows

Für die Anzahl an Duplikaten benutze aber besser count().

Columns

Es gibt vier verschiedene Verben, die Spalten beeinträchtigen, ohne die Zeilen zu vertauschen:
- mutate() - select() - rename()
- relocate()

mutate()

mutate() fügt eine neue Spalte hinzu, die aus den bereits existierenden berechnet wird. Wir berechnen einfache Dinge, wie Differenzen und Quotienten, z.B. wie lange ein Flug in der Luft war oder die Geschwindigkeit in Meilen pro Stunde.

flights |> 
  mutate(
    gain = dep_delay - arr_delay,
    speed = distance / air_time * 60
  )
## # A tibble: 336,776 × 21
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 336,766 more rows
## # ℹ 13 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>, gain <dbl>, speed <dbl>

mutate() hängt diese neuen Variablen rechts an die Tabellen dran. Mit .before können wir die Variablen auch links dran setzen.

flights |> 
  mutate(
    gain = dep_delay - arr_delay,
    speed = distance / air_time * 60,
    .before = 1
  )
## # A tibble: 336,776 × 21
##     gain speed  year month   day dep_time sched_dep_time dep_delay arr_time
##    <dbl> <dbl> <int> <int> <int>    <int>          <int>     <dbl>    <int>
##  1    -9  370.  2013     1     1      517            515         2      830
##  2   -16  374.  2013     1     1      533            529         4      850
##  3   -31  408.  2013     1     1      542            540         2      923
##  4    17  517.  2013     1     1      544            545        -1     1004
##  5    19  394.  2013     1     1      554            600        -6      812
##  6   -16  288.  2013     1     1      554            558        -4      740
##  7   -24  404.  2013     1     1      555            600        -5      913
##  8    11  259.  2013     1     1      557            600        -3      709
##  9     5  405.  2013     1     1      557            600        -3      838
## 10   -10  319.  2013     1     1      558            600        -2      753
## # ℹ 336,766 more rows
## # ℹ 12 more variables: sched_arr_time <int>, arr_delay <dbl>, carrier <chr>,
## #   flight <int>, tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>,
## #   distance <dbl>, hour <dbl>, minute <dbl>, time_hour <dttm>

Der . ist ein Zeichen, so dass .before ein Argument der Funktion ist, nicht der Name einer neuen Variable. .after kannst du auch benutzen, um nach einer Variable deine neue Spalte zu setzen. Statt der Position, kannst du hier auch den Namen der Variable setzen (z.B. day).

flights |> 
  mutate(
    gain = dep_delay - arr_delay,
    speed = distance / air_time * 60,
    .after = day
  )
## # A tibble: 336,776 × 21
##     year month   day  gain speed dep_time sched_dep_time dep_delay arr_time
##    <int> <int> <int> <dbl> <dbl>    <int>          <int>     <dbl>    <int>
##  1  2013     1     1    -9  370.      517            515         2      830
##  2  2013     1     1   -16  374.      533            529         4      850
##  3  2013     1     1   -31  408.      542            540         2      923
##  4  2013     1     1    17  517.      544            545        -1     1004
##  5  2013     1     1    19  394.      554            600        -6      812
##  6  2013     1     1   -16  288.      554            558        -4      740
##  7  2013     1     1   -24  404.      555            600        -5      913
##  8  2013     1     1    11  259.      557            600        -3      709
##  9  2013     1     1     5  405.      557            600        -3      838
## 10  2013     1     1   -10  319.      558            600        -2      753
## # ℹ 336,766 more rows
## # ℹ 12 more variables: sched_arr_time <int>, arr_delay <dbl>, carrier <chr>,
## #   flight <int>, tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>,
## #   distance <dbl>, hour <dbl>, minute <dbl>, time_hour <dttm>

Alternativ kannst du kontrollieren, welche Variablen behalten werden sollen, mit dem .keep Argument. Ein nützliches Argument ist used, welches dir die Inputs und Outputs deiner Berechnungen anzeigt.

flights |> 
  mutate(
    gain = dep_delay - arr_delay,
    hours = air_time / 60,
    gain_per_hour = gain / hours,
    .keep = "used"
  )
## # A tibble: 336,776 × 6
##    dep_delay arr_delay air_time  gain hours gain_per_hour
##        <dbl>     <dbl>    <dbl> <dbl> <dbl>         <dbl>
##  1         2        11      227    -9 3.78          -2.38
##  2         4        20      227   -16 3.78          -4.23
##  3         2        33      160   -31 2.67         -11.6 
##  4        -1       -18      183    17 3.05           5.57
##  5        -6       -25      116    19 1.93           9.83
##  6        -4        12      150   -16 2.5           -6.4 
##  7        -5        19      158   -24 2.63          -9.11
##  8        -3       -14       53    11 0.883         12.5 
##  9        -3        -8      140     5 2.33           2.14
## 10        -2         8      138   -10 2.3           -4.35
## # ℹ 336,766 more rows

select()

Oftmals kriegt man unendlich viele Variablen an die Hand, von denen uns aber nur ein paar interessieren. select() gibt uns nur einige von ihnen wieder. In unserem Beispiel haben wir nur 19 Variablen, aber die Idee zählt.

# Select columns by name
flights |> 
  select(year, month, day)
## # A tibble: 336,776 × 3
##     year month   day
##    <int> <int> <int>
##  1  2013     1     1
##  2  2013     1     1
##  3  2013     1     1
##  4  2013     1     1
##  5  2013     1     1
##  6  2013     1     1
##  7  2013     1     1
##  8  2013     1     1
##  9  2013     1     1
## 10  2013     1     1
## # ℹ 336,766 more rows
# Select all columns between year and day (inclusive)
flights |> 
  select(year:day)
## # A tibble: 336,776 × 3
##     year month   day
##    <int> <int> <int>
##  1  2013     1     1
##  2  2013     1     1
##  3  2013     1     1
##  4  2013     1     1
##  5  2013     1     1
##  6  2013     1     1
##  7  2013     1     1
##  8  2013     1     1
##  9  2013     1     1
## 10  2013     1     1
## # ℹ 336,766 more rows
# Select all columns except those from year to day (inclusive)
flights |> 
  select(!year:day)
## # A tibble: 336,776 × 16
##    dep_time sched_dep_time dep_delay arr_time sched_arr_time arr_delay carrier
##       <int>          <int>     <dbl>    <int>          <int>     <dbl> <chr>  
##  1      517            515         2      830            819        11 UA     
##  2      533            529         4      850            830        20 UA     
##  3      542            540         2      923            850        33 AA     
##  4      544            545        -1     1004           1022       -18 B6     
##  5      554            600        -6      812            837       -25 DL     
##  6      554            558        -4      740            728        12 UA     
##  7      555            600        -5      913            854        19 B6     
##  8      557            600        -3      709            723       -14 EV     
##  9      557            600        -3      838            846        -8 B6     
## 10      558            600        -2      753            745         8 AA     
## # ℹ 336,766 more rows
## # ℹ 9 more variables: flight <int>, tailnum <chr>, origin <chr>, dest <chr>,
## #   air_time <dbl>, distance <dbl>, hour <dbl>, minute <dbl>, time_hour <dttm>
# Select all columns that are characters
flights |> 
  select(where(is.character))
## # A tibble: 336,776 × 4
##    carrier tailnum origin dest 
##    <chr>   <chr>   <chr>  <chr>
##  1 UA      N14228  EWR    IAH  
##  2 UA      N24211  LGA    IAH  
##  3 AA      N619AA  JFK    MIA  
##  4 B6      N804JB  JFK    BQN  
##  5 DL      N668DN  LGA    ATL  
##  6 UA      N39463  EWR    ORD  
##  7 B6      N516JB  EWR    FLL  
##  8 EV      N829AS  LGA    IAD  
##  9 B6      N593JB  JFK    MCO  
## 10 AA      N3ALAA  LGA    ORD  
## # ℹ 336,766 more rows

Viele weitere Hilfsfunktionen arbeiten mit select():

# starts_with("tai"): matches names that begin with “tai”
flights |> 
    select(starts_with("tai"))
## # A tibble: 336,776 × 1
##    tailnum
##    <chr>  
##  1 N14228 
##  2 N24211 
##  3 N619AA 
##  4 N804JB 
##  5 N668DN 
##  6 N39463 
##  7 N516JB 
##  8 N829AS 
##  9 N593JB 
## 10 N3ALAA 
## # ℹ 336,766 more rows
  • starts_with("abc")
  • ends_with("xyz")
  • contains("ijk")
  • num_range("x", 1:3): matcht x1, x2, x3

Variablen neu benennen über select() durch =.

flights |> 
  select(tail_num = tailnum)
## # A tibble: 336,776 × 1
##    tail_num
##    <chr>   
##  1 N14228  
##  2 N24211  
##  3 N619AA  
##  4 N804JB  
##  5 N668DN  
##  6 N39463  
##  7 N516JB  
##  8 N829AS  
##  9 N593JB  
## 10 N3ALAA  
## # ℹ 336,766 more rows

rename()

flights |> 
  rename(tail_num = tailnum)
## # A tibble: 336,776 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 336,766 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tail_num <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Funktioniert wie select(), aber behält alle nicht ausgewähle Variablen bei.

relocate()

Bewege Variablen nach vorne.

flights |> 
  relocate(time_hour, air_time)
## # A tibble: 336,776 × 19
##    time_hour           air_time  year month   day dep_time sched_dep_time
##    <dttm>                 <dbl> <int> <int> <int>    <int>          <int>
##  1 2013-01-01 05:00:00      227  2013     1     1      517            515
##  2 2013-01-01 05:00:00      227  2013     1     1      533            529
##  3 2013-01-01 05:00:00      160  2013     1     1      542            540
##  4 2013-01-01 05:00:00      183  2013     1     1      544            545
##  5 2013-01-01 06:00:00      116  2013     1     1      554            600
##  6 2013-01-01 05:00:00      150  2013     1     1      554            558
##  7 2013-01-01 06:00:00      158  2013     1     1      555            600
##  8 2013-01-01 06:00:00       53  2013     1     1      557            600
##  9 2013-01-01 06:00:00      140  2013     1     1      557            600
## 10 2013-01-01 06:00:00      138  2013     1     1      558            600
## # ℹ 336,766 more rows
## # ℹ 12 more variables: dep_delay <dbl>, arr_time <int>, sched_arr_time <int>,
## #   arr_delay <dbl>, carrier <chr>, flight <int>, tailnum <chr>, origin <chr>,
## #   dest <chr>, distance <dbl>, hour <dbl>, minute <dbl>

Oder an einen bestimmten Ort mit .before und .after.

flights |> 
  relocate(year:dep_time, .after = time_hour)
## # A tibble: 336,776 × 19
##    sched_dep_time dep_delay arr_time sched_arr_time arr_delay carrier flight
##             <int>     <dbl>    <int>          <int>     <dbl> <chr>    <int>
##  1            515         2      830            819        11 UA        1545
##  2            529         4      850            830        20 UA        1714
##  3            540         2      923            850        33 AA        1141
##  4            545        -1     1004           1022       -18 B6         725
##  5            600        -6      812            837       -25 DL         461
##  6            558        -4      740            728        12 UA        1696
##  7            600        -5      913            854        19 B6         507
##  8            600        -3      709            723       -14 EV        5708
##  9            600        -3      838            846        -8 B6          79
## 10            600        -2      753            745         8 AA         301
## # ℹ 336,766 more rows
## # ℹ 12 more variables: tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>,
## #   distance <dbl>, hour <dbl>, minute <dbl>, time_hour <dttm>, year <int>,
## #   month <int>, day <int>, dep_time <int>
flights |> 
  relocate(starts_with("arr"), .before = dep_time)
## # A tibble: 336,776 × 19
##     year month   day arr_time arr_delay dep_time sched_dep_time dep_delay
##    <int> <int> <int>    <int>     <dbl>    <int>          <int>     <dbl>
##  1  2013     1     1      830        11      517            515         2
##  2  2013     1     1      850        20      533            529         4
##  3  2013     1     1      923        33      542            540         2
##  4  2013     1     1     1004       -18      544            545        -1
##  5  2013     1     1      812       -25      554            600        -6
##  6  2013     1     1      740        12      554            558        -4
##  7  2013     1     1      913        19      555            600        -5
##  8  2013     1     1      709       -14      557            600        -3
##  9  2013     1     1      838        -8      557            600        -3
## 10  2013     1     1      753         8      558            600        -2
## # ℹ 336,766 more rows
## # ℹ 11 more variables: sched_arr_time <int>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Gruppen

dplyr wird noch mächtiger durch die Arbeit mit Gruppen.

group_by

Benutze group_by() um deinen Datensatz in Gruppen zu unterteilen, die dir bei deiner Analyse helfen.

flights |> 
  group_by(month)
## # A tibble: 336,776 × 19
## # Groups:   month [12]
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 336,766 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Die Daten werden nicht verändert, aber der Output ist jetzt grouped by month. grouped_by() verändert das Verhalten nachkommender Verben.

summarize()

Die wichtigste gruppierte Operation ist eine Zusammenfassung (summary), welche jede Gruppe zu einer einzelnen Zeile zusammenfasst. Die durchschnittliche departure delay (Abflugverzögerung) nach Monaten kann so berechnet werden.

flights |> 
  group_by(month) |> 
  summarize(
    delay = mean(dep_delay, na.rm = TRUE)
  )
## # A tibble: 12 × 2
##    month delay
##    <int> <dbl>
##  1     1 10.0 
##  2     2 10.8 
##  3     3 13.2 
##  4     4 13.9 
##  5     5 13.0 
##  6     6 20.8 
##  7     7 21.7 
##  8     8 12.6 
##  9     9  6.72
## 10    10  6.24
## 11    11  5.44
## 12    12 16.6

In einem summarize() Aufruf kannst du jede beliebige Anzahl von Summaries ausgeben lassen. n() gibt die Anzahl von Zeilen in jeder Gruppe wieder.

flights |> 
  group_by(month) |> 
  summarize(
    delay = mean(dep_delay, na.rm = TRUE), 
    n = n()
  )
## # A tibble: 12 × 3
##    month delay     n
##    <int> <dbl> <int>
##  1     1 10.0  27004
##  2     2 10.8  24951
##  3     3 13.2  28834
##  4     4 13.9  28330
##  5     5 13.0  28796
##  6     6 20.8  28243
##  7     7 21.7  29425
##  8     8 12.6  29327
##  9     9  6.72 27574
## 10    10  6.24 28889
## 11    11  5.44 27268
## 12    12 16.6  28135

slice_ Funktionen

Fünf Funktionen erlauben es dir spezielle Zeilen innerhalb jeder Gruppe wiederzugeben.

  • df |> slice_head(n = 1) erste Zeile jeder Gruppe
  • df |> slice_tail(n = 1) letzte Zeile jeder Gruppe
  • df |> slice_min(x, n = 1) Zeile mit kleinstem Wert von x
  • df |> slice_max(x, n = 1) Zeile mit größtem Wert
  • df |> slice_sample(n = 1) zufällige Zeile

Über n kannst du auch mehr als eine Zeile dir ausgeben lassen. Stattdessen kannst du dir auch einen Prozentsatz ausgeben lassen: prop = 0.1 für 10%.

flights |> 
  group_by(dest) |> 
  slice_max(arr_delay, n = 1)
## # A tibble: 108 × 19
## # Groups:   dest [105]
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     7    22     2145           2007        98      132           2259
##  2  2013     7    23     1139            800       219     1250            909
##  3  2013     1    25      123           2000       323      229           2101
##  4  2013     8    17     1740           1625        75     2042           2003
##  5  2013     7    22     2257            759       898      121           1026
##  6  2013     7    10     2056           1505       351     2347           1758
##  7  2013     8    13     1156            832       204     1417           1029
##  8  2013     2    21     1728           1316       252     1839           1413
##  9  2013    12     1     1504           1056       248     1628           1230
## 10  2013     4    10       25           1900       325      136           2045
## # ℹ 98 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Eine ähnliche Ausgabe kriegst du über summarize(). Nur einmal kriegst du die ganze Zeile, einmal die single summary.

flights |> 
  group_by(dest) |> 
  summarize(max_delay = max(arr_delay, na.rm = TRUE))
## Warning: There was 1 warning in `summarize()`.
## ℹ In argument: `max_delay = max(arr_delay, na.rm = TRUE)`.
## ℹ In group 52: `dest = "LGA"`.
## Caused by warning in `max()`:
## ! kein nicht-fehlendes Argument für max; gebe -Inf zurück
## # A tibble: 105 × 2
##    dest  max_delay
##    <chr>     <dbl>
##  1 ABQ         153
##  2 ACK         221
##  3 ALB         328
##  4 ANC          39
##  5 ATL         895
##  6 AUS         349
##  7 AVL         228
##  8 BDL         266
##  9 BGR         238
## 10 BHM         291
## # ℹ 95 more rows

Gruppieren nach mehreren Variablen

Du kannst natürlich auch nach mehreren Variablen gruppieren. Eine Gruppe für jeden Tag.

daily <- flights |>  
  group_by(year, month, day)
daily
## # A tibble: 336,776 × 19
## # Groups:   year, month, day [365]
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 336,766 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Wenn du ein tibble zusammenfasst, gruppiert mit mehr als einer Variable, kriegst du einen Hinweis nach welcher Variable vor der letzten zuvor gruppiert wurde.

daily_flights <- daily |> 
  summarize(
    n = n()
  )
## `summarise()` has grouped output by 'year', 'month'. You can override using the
## `.groups` argument.
daily_flights |>
  print(n = 30)
## # A tibble: 365 × 4
## # Groups:   year, month [12]
##     year month   day     n
##    <int> <int> <int> <int>
##  1  2013     1     1   842
##  2  2013     1     2   943
##  3  2013     1     3   914
##  4  2013     1     4   915
##  5  2013     1     5   720
##  6  2013     1     6   832
##  7  2013     1     7   933
##  8  2013     1     8   899
##  9  2013     1     9   902
## 10  2013     1    10   932
## 11  2013     1    11   930
## 12  2013     1    12   690
## 13  2013     1    13   828
## 14  2013     1    14   928
## 15  2013     1    15   894
## 16  2013     1    16   901
## 17  2013     1    17   927
## 18  2013     1    18   924
## 19  2013     1    19   674
## 20  2013     1    20   786
## 21  2013     1    21   912
## 22  2013     1    22   890
## 23  2013     1    23   897
## 24  2013     1    24   925
## 25  2013     1    25   922
## 26  2013     1    26   680
## 27  2013     1    27   823
## 28  2013     1    28   923
## 29  2013     1    29   890
## 30  2013     1    30   900
## # ℹ 335 more rows

Diese Meldung kannst du unterdrücken durch:

daily_flights <- daily |> 
  summarize(
    n = n(), 
    .groups = "drop_last"
  )

Und erhälst so dann:

daily_flights |>
  print(n = 5)
## # A tibble: 365 × 4
## # Groups:   year, month [12]
##    year month   day     n
##   <int> <int> <int> <int>
## 1  2013     1     1   842
## 2  2013     1     2   943
## 3  2013     1     3   914
## 4  2013     1     4   915
## 5  2013     1     5   720
## # ℹ 360 more rows

Ungrouping

Durch ungroup() kannst du die Gruppierung außerhalb von summarize() entfernen.

daily |> 
  ungroup() |>
  summarize(
    delay = mean(dep_delay, na.rm = TRUE), 
    flights = n()
  )
## # A tibble: 1 × 2
##   delay flights
##   <dbl>   <int>
## 1  12.6  336776

Wenn du einen ungruppierten Data Frame zusammenfasst, kriegst du logischerweise nur eine Zeile als Ausgabe. Als hättest du nur eine Gruppe.

Case Study

Bei Aggregation ist es immer sinnvoll eine count Variable mit einzubauen.
Schauen wir uns die Flugzeuge an, identifiziert durch ihre tail number, die die höchsten durchschnittlichen Verspätungen haben.

delays <- flights |>  
  filter(!is.na(arr_delay), !is.na(tailnum)) |> 
  group_by(tailnum) |> 
  summarize(
    delay = mean(arr_delay, na.rm = TRUE),
    n = n()
  )

ggplot(delays, aes(x = delay)) + 
  geom_freqpoly(binwidth = 10)

Manche Flugzeuge haben einen durchschnittlichen delay von 300 Minuten.
Erstellen wir einen Scatterplot mit Anzahl an Flügen vs. average delay:

ggplot(delays, aes(x = n, y = delay)) + 
  geom_point(alpha = 1/10)

Gruppen mit den kleinsten Anzahlen an Beobachtungen wollen wir rausfiltern, sodass mehr Pattern und weniger extreme Variationen in kleinen Gruppen angezeigt werden.

delays |>  
  filter(n > 25) |> 
  ggplot(aes(x = n, y = delay)) + 
  geom_point(alpha = 1/10) + 
  geom_smooth(se = FALSE)
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'

Beispiel_2 Baseball.

Aus dem Lahman Paket: Anteil an Versuchen, bei denen der Ball getroffen wurde vs. Anzahl an Versuche.

batters <- Lahman::Batting |> 
  group_by(playerID) |> 
  summarize(
    perf = sum(H, na.rm = TRUE) / sum(AB, na.rm = TRUE),
    n = sum(AB, na.rm = TRUE)
  )
batters
## # A tibble: 20,166 × 3
##    playerID    perf     n
##    <chr>      <dbl> <int>
##  1 aardsda01 0          4
##  2 aaronha01 0.305  12364
##  3 aaronto01 0.229    944
##  4 aasedo01  0          5
##  5 abadan01  0.0952    21
##  6 abadfe01  0.111      9
##  7 abadijo01 0.224     49
##  8 abbated01 0.254   3044
##  9 abbeybe01 0.169    225
## 10 abbeych01 0.281   1756
## # ℹ 20,156 more rows

Zwei Muster sind zu erkennen: mehr Datenpunkte heißt weniger Variation. Und eine positive Korrelation zwischen perf und n ist zu erkennen. Logisch, da der beste Batter auch die meisten Möglichkeiten kriegen soll.

batters |> 
  filter(n > 100) |> 
  ggplot(aes(x = n, y = perf)) +
    geom_point(alpha = 1 / 10) + 
    geom_smooth(se = FALSE)
## `geom_smooth()` using method = 'gam' and formula = 'y ~ s(x, bs = "cs")'

Sortierst du naiv nach desc(ba) (batting average), sind oben die glücklichsten, nicht die besten Spieler zu finden.

batters |> 
  arrange(desc(perf))
## # A tibble: 20,166 × 3
##    playerID   perf     n
##    <chr>     <dbl> <int>
##  1 abramge01     1     1
##  2 alberan01     1     1
##  3 banisje01     1     1
##  4 bartocl01     1     1
##  5 bassdo01      1     1
##  6 birasst01     1     2
##  7 bruneju01     1     1
##  8 burnscb01     1     1
##  9 cammaer01     1     1
## 10 campsh01      1     1
## # ℹ 20,156 more rows

Pipes

Die Pipe |> ist ein mächtiges Werkzeug, die eine Reihe von Operationen ausdrückt, die ein Objekt transformieren. Bekannt ist vielleicht der Vorgänger %>%.

Für den keyboard shortcut Ctrl + Shift + M, gehe in Options, Editing und klicke Use native pipe operator an.

Warum Pipe benutzen?

Jedes dplyr verb ist sehr simpel, jedoch erfordert die Lösung komplexer Probleme eine Kombination vieler Verben.

flights |>  
  filter(!is.na(arr_delay), !is.na(tailnum)) |> 
  group_by(tailnum) |> 
  summarize(
    delay = mean(arr_delay, na.rm = TRUE),
    n = n()
  )
## # A tibble: 4,037 × 3
##    tailnum  delay     n
##    <chr>    <dbl> <int>
##  1 D942DN  31.5       4
##  2 N0EGMQ   9.98    352
##  3 N10156  12.7     145
##  4 N102UW   2.94     48
##  5 N103US  -6.93     46
##  6 N104UW   1.80     46
##  7 N10575  20.7     269
##  8 N105UW  -0.267    45
##  9 N107US  -5.73     41
## 10 N108UW  -1.25     60
## # ℹ 4,027 more rows

Die Verben stehen am Beginn jeder Zeile:
flights Daten, dann filter, dann gruppieren, dann zusammenfassen.

Ohne Pipe:

summarize(
  group_by(
    filter(
      flights, 
      !is.na(arr_delay), !is.na(tailnum)
    ),
    tailnum
  ), 
  delay = mean(arr_delay, na.rm = TRUE
  ), 
  n = n()
)
## # A tibble: 4,037 × 3
##    tailnum  delay     n
##    <chr>    <dbl> <int>
##  1 D942DN  31.5       4
##  2 N0EGMQ   9.98    352
##  3 N10156  12.7     145
##  4 N102UW   2.94     48
##  5 N103US  -6.93     46
##  6 N104UW   1.80     46
##  7 N10575  20.7     269
##  8 N105UW  -0.267    45
##  9 N107US  -5.73     41
## 10 N108UW  -1.25     60
## # ℹ 4,027 more rows

magrittr und die %>% Pipe

Im magrittr Paket wird die %>% Pipe angeboten. Diese Pipe kannst du benutzen, wenn das tidyverse Paket geladen ist.

library(tidyverse)

mtcars %>% 
  group_by(cyl) %>%
  summarize(n = n())
## # A tibble: 3 × 2
##     cyl     n
##   <dbl> <int>
## 1     4    11
## 2     6     7
## 3     8    14

|> vs. %>%

Die Pipe übergibt das Objekt zu ihrer linken Seite als erstes Argument auf die rechte Seite. %>% erlaubt es die Platzierung zu tauschen:

  • x %>% f(1) ist äquivalent zu f(x, 1), aber x %>% f(1, .) ist äquivalent zu f(1, x).
    Bei der “neuen Pipe” muss das Argument genannt werden. x |> f(1, y = _) ist äquivalent zu f(1, y = x).

  • Mit %>% kannst du . auf der linken Seite von Operatoren wie $, [[ oder [ benutzen, so dass du z.B. eine einzelne Spalte extrahieren kannst: mtcars %>% .$cyl.
    Du kannst sogar zwei mal ersetzen: df %>% {split(.$x, .$y)} ist äquivalent zu split(df$x, df$y).
    Für das Extrahieren einer Spalte benutze statt pull(mtcars, cyl) die pull Funktion dplyr::pull():

mtcars |> pull(cyl)
##  [1] 6 6 4 6 8 6 8 4 4 6 6 8 8 8 8 8 8 4 4 4 4 8 8 8 8 4 4 4 8 6 8 4
  • %>% erlaubt es dir bei Funktionen die Klamern wegzulassen, wenn es keine Argumente gibt: mtcars[,1] %>% mean funktioniert, mtcars[,1] |> mean aber nicht.

|> vs. +

Da ggplot2 vor der Pipe entwickelt wurde, ist der Übergang von |> zu + notwendig:

ggplot2::diamonds |> 
  count(cut, clarity) |> 
  ggplot(aes(x = clarity, y = cut, fill = n)) + 
  geom_tile()

Vor den zwei Doppelpunkten steht das Paket aus dem der Datensatz kommt! Gleiche Ergebnisse liefert übrigens:

aa<-count(ggplot2::diamonds, cut, clarity)
ggplot(aa, aes(x = clarity, y = cut, fill = n)) +
  geom_tile()

Code Style

Ohne guten Code style kannst du arbeiten, mit ist es aber wesentlich einfacher. Es gibt sogar Pakete mit denen du deinen Code aufhübschen kannst. Zum Beispiel das styler Paket. Nach der Installation benutze Cmd + Shift + P und gebe styler ein, um alle Shortcuts zu sehen.

Namen

Kleinbuchstaben, Zahlen und Trennungen durch _ werden als guter Style angesehen.

# Strebe nach:
short_flights <- flights |> filter(air_time < 60)

# Vermeide:
SHORTFLIGHTS <- flights |> filter(air_time < 60)

Lange, beschreibende Namen sind einfach zu verstehen. Kurze sind schneller zu tippen, aber auch eventuell später schwer zu verstehen.

Lücken

Lücken bei mathematischen Operatoren erleichtern das Lesen (+, -, ==, <). Nicht bei ^, aber um Zuweisungen. Keine Lücken stehen nach oder vor Klammern, aber nach dem ,.

# bitte
z <- (a + b)^2 / d

# nicht
z<-( a + b ) ^ 2/d

# bitte
mean(x, na.rm = TRUE)

# nicht
mean (x ,na.rm=TRUE)

Extra Lücken einzufügen, so dass es der Lesbarkeit und Harmonie dient, ist durchaus wünschenswert.

flights |> 
  mutate(
    speed      = air_time / distance,
    dep_hour   = dep_time %/% 100,
    dep_minute = dep_time %%  100
  )

Pipes

Pipes sollten immer das letzte Element einer Zeile sein.

# bitte 
flights |>  
  filter(!is.na(arr_delay), !is.na(tailnum)) |> 
  count(dest)

# nicht
flights|>filter(!is.na(arr_delay), !is.na(tailnum))|>count(dest)

Hat die piping function Argumente wie mutate() oder summarize(), so kommt jedes Argument in eine neue Zeile.

# bitte
flights |>  
  group_by(tailnum) |> 
  summarize(
    delay = mean(arr_delay, na.rm = TRUE),
    n = n()
  )

# nicht
flights |>
  group_by(
    tailnum
  ) |> 
  summarize(delay = mean(arr_delay, na.rm = TRUE), n = n())

Nach der Zeile der Pipe wird zwei Leerzeichen eingerückt. Bekommt jedes Argument eine eigene Zeile wird wieder eingerückt. Die ) bekommt wieder eine eigene Zeile, auf Höhe des Funktionsnamens.

# bitte
flights |>  
  group_by(tailnum) |> 
  summarize(
    delay = mean(arr_delay, na.rm = TRUE),
    n = n()
  )

# nicht
flights|>
  group_by(tailnum) |> 
  summarize(
             delay = mean(arr_delay, na.rm = TRUE), 
             n = n()
           )

flights|>
  group_by(tailnum) |> 
  summarize(
  delay = mean(arr_delay, na.rm = TRUE), 
  n = n()
  )

Manchmal passt der komplette Befehl in eine Zeile, jedoch wächst erfahrungsgemäß die Zeile schnell an.

# alles passt kompakt in eine Zeile
df |> mutate(y = x + 1)

# vier mal so viel Zeilen, jedoch einfach zu verlängern 
# mehr Variable und Schritte in der Zukunft.
df |> 
  mutate(
    y = x + 1
  )

Vermeide es zu lange Pipes von mehr als 10-15 Zeilen zu schreiben. Breche sie runter in Unteraufgaben und verpasse ihnen günstige Namen. Wichtig.

ggplot2

Was für |> bei der Pipe gilt, gilt natürlich auch für + bei ggplot2.

flights |> 
  group_by(month) |> 
  summarize(
    delay = mean(arr_delay, na.rm = TRUE)
  ) |> 
  ggplot(aes(x = month, y = delay)) +
  geom_point() + 
  geom_line()

Jedes Argument kommt in ihre eigene Zeile.

flights |> 
  group_by(dest) |> 
  summarize(
    distance = mean(distance),
    speed = mean(air_time / distance, na.rm = TRUE)
  ) |> 
  ggplot(aes(x = distance, y = speed)) +
  geom_smooth(
    method = "loess",
    span = 0.5,
    se = FALSE, 
    color = "white", 
    size = 4
  ) +
  geom_point()

Sectioning Comments

Wird dein Skript länger, so trenne es in kleinere Happen. Verpasse Kommentare und hebe sie optisch hervor.

# Load data --------------------------------------

# Plot data --------------------------------------

Mit Cmd + Shift + R werden sie unten links im Editor angezeigt, in einer drop-down Liste.

Daten importieren

Einleitung

Mit den von R bereitgestellten Daten zu arbeiten hilft ungemein, aber für eigene Projekte wollen wir natürlich auch mal eigene Daten importieren.

Voraussetzungen

Dazu brauchen wit das readr Paket, welches Teil des tidyverse ist.

library(tidyverse)

Daten aus Datei einlesen

Zu Beginn konzentrieren wir uns auf eine .csv Datei:
In der ersten Zeile stehen für gewöhnlich die Spaltennamen, danach kommen die Daten.

#> Student ID, Full Name,Age
#> 1,Sunil Huffmann,4
#> 2,Baclay Lynn,5
#> 3,Peter Pan,7
#> 4,Leon Leonidas,
#> 5,Marion Farre,five
#> 6,Attila Attilon,6

Diese Daten können wir aus der Datei einlesen mit read_csv(). Zwei Spalten haben wir noch hinzugefügt

students <- read_csv("data/students.csv")
## Rows: 6 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (4): Full Name, favourite.food, mealPlan, AGE
## dbl (1): Student ID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Bei Ausgabe wird die Anzahl der Zeilen und Spalten genannt, das Trennzeichen und die Spalten-Spezifikationen: Namen und Datentyp.

Praktische Hinweise

Nach dem Einlesen werden die Daten für gewöhnlich transformiert, so dass wir mit ihnen einfacher arbeiten können. In der favourite.food Spalte haben wir den Character String N/A, den wir in ein “richtiges” NA, not available, transformieren wollen.

students <- read_csv("data/students.csv", na = c("N/A", ""))
## Rows: 6 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (4): Full Name, favourite.food, mealPlan, AGE
## dbl (1): Student ID
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
students
## # A tibble: 6 × 5
##   `Student ID` `Full Name`    favourite.food     mealPlan            AGE  
##          <dbl> <chr>          <chr>              <chr>               <chr>
## 1            1 Sunil Huffmann Strawberry yoghurt Lunch only          4    
## 2            2 Baclay Lynn    French Fries       Lunch only          5    
## 3            3 Peter Pan      <NA>               Breakfast and Lunch 7    
## 4            4 Leon Leonidas  Anchovies          Lunch only          <NA> 
## 5            5 Marion Farre   Pizza              Breakfast and Lunch five 
## 6            6 Attila Attilon Ice cream          Lunch only          6

Student ID und Full Name sind umgeben von Backticks. Das passiert, weil es Leerzeichen in den Variablennamen gibt, so dass die Namensregeln für Variablen gebrochen werden. Wir können natürlich die Variablennamen umbenennen.

students |> 
  rename(
    student_id = `Student ID`,
    full_name = `Full Name`
  )
## # A tibble: 6 × 5
##   student_id full_name      favourite.food     mealPlan            AGE  
##        <dbl> <chr>          <chr>              <chr>               <chr>
## 1          1 Sunil Huffmann Strawberry yoghurt Lunch only          4    
## 2          2 Baclay Lynn    French Fries       Lunch only          5    
## 3          3 Peter Pan      <NA>               Breakfast and Lunch 7    
## 4          4 Leon Leonidas  Anchovies          Lunch only          <NA> 
## 5          5 Marion Farre   Pizza              Breakfast and Lunch five 
## 6          6 Attila Attilon Ice cream          Lunch only          6

Eine Alternatie ist es janitor::clean_names() zu benutzen, so dass heuristisch alle Namen in angenehme Formate gebracht werden.

library(janitor)
## Warning: Paket 'janitor' wurde unter R Version 4.2.3 erstellt
## 
## Attache Paket: 'janitor'
## Die folgenden Objekte sind maskiert von 'package:stats':
## 
##     chisq.test, fisher.test
students |> janitor::clean_names()
## # A tibble: 6 × 5
##   student_id full_name      favourite_food     meal_plan           age  
##        <dbl> <chr>          <chr>              <chr>               <chr>
## 1          1 Sunil Huffmann Strawberry yoghurt Lunch only          4    
## 2          2 Baclay Lynn    French Fries       Lunch only          5    
## 3          3 Peter Pan      <NA>               Breakfast and Lunch 7    
## 4          4 Leon Leonidas  Anchovies          Lunch only          <NA> 
## 5          5 Marion Farre   Pizza              Breakfast and Lunch five 
## 6          6 Attila Attilon Ice cream          Lunch only          6

Eine andere gewöhnliche Aufgabe ist es den Variablentyp zu bedenken. Zum Beispiel ist meal_type eine kategoriale Variable mit einer bekannten Menge an möglichen Werten, die in R als Faktoren dargestellt werden sollten.

students |>
  janitor::clean_names() |>
  mutate(
    meal_plan = factor(meal_plan)
  )
## # A tibble: 6 × 5
##   student_id full_name      favourite_food     meal_plan           age  
##        <dbl> <chr>          <chr>              <fct>               <chr>
## 1          1 Sunil Huffmann Strawberry yoghurt Lunch only          4    
## 2          2 Baclay Lynn    French Fries       Lunch only          5    
## 3          3 Peter Pan      <NA>               Breakfast and Lunch 7    
## 4          4 Leon Leonidas  Anchovies          Lunch only          <NA> 
## 5          5 Marion Farre   Pizza              Breakfast and Lunch five 
## 6          6 Attila Attilon Ice cream          Lunch only          6

Die Werte sind gleich geblieben, nur der Typ der Variablen (unterhalb der Namen) hat sich von Character <chr> zu Faktor <fct> geändert.

Als nächstes wollen wir noch die age Spalte reparieren. Sein Typ ist Character, weil in einer Zeile five, statt 5 steht.

students <- students |>
  janitor::clean_names() |>
  mutate(
    meal_plan = factor(meal_plan),
    age = parse_number(if_else(age == "five", "5", age))
  )

students
## # A tibble: 6 × 5
##   student_id full_name      favourite_food     meal_plan             age
##        <dbl> <chr>          <chr>              <fct>               <dbl>
## 1          1 Sunil Huffmann Strawberry yoghurt Lunch only              4
## 2          2 Baclay Lynn    French Fries       Lunch only              5
## 3          3 Peter Pan      <NA>               Breakfast and Lunch     7
## 4          4 Leon Leonidas  Anchovies          Lunch only             NA
## 5          5 Marion Farre   Pizza              Breakfast and Lunch     5
## 6          6 Attila Attilon Ice cream          Lunch only              6

Andere Argumente

Ein Trick am Anfang: read_csv() kann in einem String kreierte csv Dateien lesen:

read_csv(
  "a,b,c
  1,2,3
  4,5,6"
)
## Rows: 2 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (3): a, b, c
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
## # A tibble: 2 × 3
##       a     b     c
##   <dbl> <dbl> <dbl>
## 1     1     2     3
## 2     4     5     6

Die erste Zeile wird für die Variablen benutzt. Manchmal finden wir noch davor Metadaten, die du durch skip = n weglassen kannst. In diesem Fall die ersten n Zeilen. Oder benutze Kommentare wie folgt:

read_csv(
  "The first line of metadata
  The second line of metadata
  x,y,z
  1,2,3",
  skip = 2
)
## Rows: 1 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (3): x, y, z
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
## # A tibble: 1 × 3
##       x     y     z
##   <dbl> <dbl> <dbl>
## 1     1     2     3
read_csv(
  "# A comment I want to skip
  x,y,z
  1,2,3",
  comment = "#"
)
## Rows: 1 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (3): x, y, z
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
## # A tibble: 1 × 3
##       x     y     z
##   <dbl> <dbl> <dbl>
## 1     1     2     3

Manchmal haben Daten keine Spaltennamen. Durch col_names = FALSE teilst du dies R mit, so dass die erste Zeile nicht als Kopf benutzt wird, sondern R automatisch Spaltennamen von X1 bis Xn ausgibt.

read_csv(
  "1,2,3
  4,5,6",
  col_names = FALSE
)
## Rows: 2 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (3): X1, X2, X3
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
## # A tibble: 2 × 3
##      X1    X2    X3
##   <dbl> <dbl> <dbl>
## 1     1     2     3
## 2     4     5     6

Einen Character Vektor mit Spaltennamen kannst du natürlich auch an die Tabelle übergeben.

read_csv(
  "1,2,3
  4,5,6",
  col_names = c("x", "y", "z")
)
## Rows: 2 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (3): x, y, z
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
## # A tibble: 2 × 3
##       x     y     z
##   <dbl> <dbl> <dbl>
## 1     1     2     3
## 2     4     5     6

Für mehr Informationen, schau in der Dokumentation von read_csv() nach.

Andere Dateitypen

Hast du bis jetzt alles verstanden, sind andere Typen unkompliziert: read_csv2(), read_tsv(), read_delim(), read_fwf(), read_table(), read_log().

Spaltentypen kontrollieren

Der Typ jeder Variable wird von readr geraten, da eine CSV keine Informationen liefert.

Guessing Types

Für das Raten wird eine Heuristik von readr genutzt. Es geht die Reihen durch:

  • Gibt es F, T, FALSE, TRUE? Wenn ja, dann ist der Typ logical.

  • Gibt es Zahlen? Dann number.

  • Matcht es dem ISO8601 Standard? Dann date oder date-time.

  • Ansonsten muss es wohl ein String sein.

read_csv("
  logical,numeric,date,string
  TRUE,1,2021-01-15,abc
  false,4.5,2021-02-15,def
  T,Inf,2021-02-16,ghi"
)
## Rows: 3 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr  (1): string
## dbl  (1): numeric
## lgl  (1): logical
## date (1): date
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
## # A tibble: 3 × 4
##   logical numeric date       string
##   <lgl>     <dbl> <date>     <chr> 
## 1 TRUE        1   2021-01-15 abc   
## 2 FALSE       4.5 2021-02-15 def   
## 3 TRUE      Inf   2021-02-16 ghi

Bei einem “netten” Datensatz funktioniert es. Ansonsten nicht.

Missing Values, Spaltentypen und Probleme

Erscheinen unerwartete Werte, so erhalten wir oft einen Character als Datentyp. Oft ist ein NA dafür verantwortlich. Schauen wir uns ein einfaches Problem an.

csv <- "
  x
  10
  .
  20
  30"

Einlesen ergibt:

df <- read_csv(csv)
## Rows: 4 Columns: 1
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (1): x
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Der Missing Value ist schnell gefunden .. Bei Tausenden von Reihen fällt das Aufspüren aber schwerer.
Wir können readr sagen, dass x eine numerische Spalte ist und dann schauen wo es Probleme gibt. Dies geschieht mit dem col_types Argument, das eine benannte Liste entgegen nimmt.

df <- read_csv(csv, col_types = list(x = col_double()))
## Warning: One or more parsing issues, call `problems()` on your data frame for details,
## e.g.:
##   dat <- vroom(...)
##   problems(dat)

Ein Problem wird angezeigt. Mehr können wir herausfinden mit problems():

problems(df)
## # A tibble: 1 × 5
##     row   col expected actual file                                              
##   <int> <int> <chr>    <chr>  <chr>                                             
## 1     3     1 a double .      C:/Users/nikla/AppData/Local/Temp/RtmpaGryD9/file…

readr erwartete ein double, aber bekam ein .. Das lässt vermuten, dass der Dataset . für Missing Values benutzt. Wir setzen dafür na = ".".

df <- read_csv(csv, na = ".")
## Rows: 4 Columns: 1
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dbl (1): x
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Spaltentypen

readr bietet neun Spaltentypen an:

  • col_logical() und col_double() liest logicals und numbers. Sie werden selten gebraucht, da readr sie meist für uns rät.

  • col_integer() liest integers (ganze Zahlen). Sie verbrauchen nur halb so viel Platz wie doubles im Memory, sind also nicht unwichtig.

  • col_character() liest Strings.

  • col_factor(), col_date() und col_datime() kreieren Faktoren, dates und date-time.

  • col_number() ignoriert nicht-numerische Komponenten.

  • col_skip() lässt eine Spalte weg.

Überschreiben vom Spaltentyp ist möglich, durch wechseln von list() zu cols().

csv <- "
x,y,z
1,2,3"

read_csv(csv, col_types = cols(.default = col_character()))
## # A tibble: 1 × 3
##   x     y     z    
##   <chr> <chr> <chr>
## 1 1     2     3

Oder neu schreiben.

read_csv(csv, col_types = list(x = col_double(), y = col_integer(), z = col_double()))
## # A tibble: 1 × 3
##       x     y     z
##   <dbl> <int> <dbl>
## 1     1     2     3

cols_only() liest nur die gewollte Spalte ein.

read_csv(
  "x,y,z
  1,2,3",
  col_types = cols_only(x = col_character())
)
## # A tibble: 1 × 1
##   x    
##   <chr>
## 1 1

Daten aus multiplen Dateien einlesen

Hast du Sales Daten von verschiedenen Monaten in verschiedenen Dateien, so kannst du sie zusammenfügen mit read_csv().

sales_files <- c("data/01-sales.csv", "data/02-sales.csv", "data/03-sales.csv")
read_csv(sales_files, id = "file")

Der neue Parameter id fügt eine neue Spalte hinzu, der die Herkunft der Datei angibt.
Es kann mühselig sein die Namen als Liste zu schreiben, wenn man viele Dateien hat. Benutze list.files() um die Dateien für dich zu finden. Ein Muster sollte im Namen vorhanden sein.

sales_files <- list.files("data", pattern = "sales\\.csv$", full.names = TRUE)
sales_files

Dateien schreiben

readr bietet zwei Funktionen, um Daten zu schreiben: write_csv(), write_tsv(). Als Argumente brauchst du den Data Frame und den Ort.

write_csv(students, "students.csv")

Die type information geht beim Schreiben als csv verloren.

students
## # A tibble: 6 × 5
##   student_id full_name      favourite_food     meal_plan             age
##        <dbl> <chr>          <chr>              <fct>               <dbl>
## 1          1 Sunil Huffmann Strawberry yoghurt Lunch only              4
## 2          2 Baclay Lynn    French Fries       Lunch only              5
## 3          3 Peter Pan      <NA>               Breakfast and Lunch     7
## 4          4 Leon Leonidas  Anchovies          Lunch only             NA
## 5          5 Marion Farre   Pizza              Breakfast and Lunch     5
## 6          6 Attila Attilon Ice cream          Lunch only              6
write_csv(students, "data/students-2.csv")
read_csv("data/students-2.csv")
## Rows: 6 Columns: 5
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (3): full_name, favourite_food, meal_plan
## dbl (2): student_id, age
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
## # A tibble: 6 × 5
##   student_id full_name      favourite_food     meal_plan             age
##        <dbl> <chr>          <chr>              <chr>               <dbl>
## 1          1 Sunil Huffmann Strawberry yoghurt Lunch only              4
## 2          2 Baclay Lynn    French Fries       Lunch only              5
## 3          3 Peter Pan      <NA>               Breakfast and Lunch     7
## 4          4 Leon Leonidas  Anchovies          Lunch only             NA
## 5          5 Marion Farre   Pizza              Breakfast and Lunch     5
## 6          6 Attila Attilon Ice cream          Lunch only              6

Das macht CSVs ein wenig unzuverlässig für Zwischenergebnisse. Du musst die Spaltenspezifikation jedes mal aufs Neue kreieren. Es gibt zwei Alternativen:

  1. write_rds() und read_rds(). Sie lagern Daten in R’s binary Format RDS.
write_rds(students, "data/students.rds")
read_rds("data/students.rds")
## # A tibble: 6 × 5
##   student_id full_name      favourite_food     meal_plan             age
##        <dbl> <chr>          <chr>              <fct>               <dbl>
## 1          1 Sunil Huffmann Strawberry yoghurt Lunch only              4
## 2          2 Baclay Lynn    French Fries       Lunch only              5
## 3          3 Peter Pan      <NA>               Breakfast and Lunch     7
## 4          4 Leon Leonidas  Anchovies          Lunch only             NA
## 5          5 Marion Farre   Pizza              Breakfast and Lunch     5
## 6          6 Attila Attilon Ice cream          Lunch only              6
  1. Das arrow Paket mit parquet Dateien.
library(arrow)
## Warning: Paket 'arrow' wurde unter R Version 4.2.3 erstellt
## 
## Attache Paket: 'arrow'
## Das folgende Objekt ist maskiert 'package:lubridate':
## 
##     duration
## Das folgende Objekt ist maskiert 'package:utils':
## 
##     timestamp
write_parquet(students, "data/students.parquet")
read_parquet("data/students.parquet")
## # A tibble: 6 × 5
##   student_id full_name      favourite_food     meal_plan             age
##        <dbl> <chr>          <chr>              <fct>               <dbl>
## 1          1 Sunil Huffmann Strawberry yoghurt Lunch only              4
## 2          2 Baclay Lynn    French Fries       Lunch only              5
## 3          3 Peter Pan      <NA>               Breakfast and Lunch     7
## 4          4 Leon Leonidas  Anchovies          Lunch only             NA
## 5          5 Marion Farre   Pizza              Breakfast and Lunch     5
## 6          6 Attila Attilon Ice cream          Lunch only              6

Parquets sind schneller als RDS, aber brauchen das Paket.

Data Entry

Manchmal musst du ein tibble() mit der Hand schreiben.

tibble(
  x = c(1, 2, 5),
  y = c("h", "m", "g"),
  z = c(0.08, 0.83, 0.60)
)
## # A tibble: 3 × 3
##       x y         z
##   <dbl> <chr> <dbl>
## 1     1 h      0.08
## 2     2 m      0.83
## 3     5 g      0.6

Jede Spalte muss natürlich die selbe Länge haben.
tribble() liest die Daten zeilenweise ein. Spaltennamen starten mit ~, Einträge werden durch Komma getrennt.

tribble(
  ~x, ~y, ~z,
  "h", 1, 0.08,
  "m", 2, 0.83,
  "g", 5, 0.60,
)
## # A tibble: 3 × 3
##   x         y     z
##   <chr> <dbl> <dbl>
## 1 h         1  0.08
## 2 m         2  0.83
## 3 g         5  0.6

Visualisieren: Layers

Einleitung

In Kapitel 2 haben wir uns mit Plots beschäftigt. In diesem Abschnitt erweitern wir unser Wissen und lernen etwas über die geschichtete Grammatik von Grafiken.

Voraussetzung

In diesem Abschnitt konzentrieren wir uns natürlich auf ggplot2. Lade tidyverse.

library(tidyverse)

Aesthetic Mappings

Unser mpg Datensatz (Autos) hat 234 Zeilen und 11 Spalten:

ggplot2::mpg
## # A tibble: 234 × 11
##    manufacturer model      displ  year   cyl trans drv     cty   hwy fl    class
##    <chr>        <chr>      <dbl> <int> <int> <chr> <chr> <int> <int> <chr> <chr>
##  1 audi         a4           1.8  1999     4 auto… f        18    29 p     comp…
##  2 audi         a4           1.8  1999     4 manu… f        21    29 p     comp…
##  3 audi         a4           2    2008     4 manu… f        20    31 p     comp…
##  4 audi         a4           2    2008     4 auto… f        21    30 p     comp…
##  5 audi         a4           2.8  1999     6 auto… f        16    26 p     comp…
##  6 audi         a4           2.8  1999     6 manu… f        18    26 p     comp…
##  7 audi         a4           3.1  2008     6 auto… f        18    27 p     comp…
##  8 audi         a4 quattro   1.8  1999     4 manu… 4        18    26 p     comp…
##  9 audi         a4 quattro   1.8  1999     4 auto… 4        16    25 p     comp…
## 10 audi         a4 quattro   2    2008     4 manu… 4        20    28 p     comp…
## # ℹ 224 more rows

Interessante Variablen für uns sind:

  1. displ: Hubraum in Liter. Numerische Variable.

  2. hwy: Treibstoffeffizienz in Meilen pro Gallone. Numerische Variable.

  3. class: Autotyp. Kategoriale Variable.

Mehr zu mpg auf der entsprechenden Hilfsseite über ?mpg.

Zuerst wollen wir die Beziehung zwischen displ und hwy visualisieren, für die verschiedenen Klassen von Autos. Ein Scatterplot mit x und y aes und der kategorialen Variable als color oder shape überrascht hier weniger.

# Left
ggplot(mpg, aes(x = displ, y = hwy, color = class)) +
  geom_point()

# Right
ggplot(mpg, aes(x = displ, y = hwy, shape = class)) +
  geom_point()
## Warning: The shape palette can deal with a maximum of 6 discrete values because
## more than 6 becomes difficult to discriminate; you have 7. Consider
## specifying shapes manually if you must have them.
## Warning: Removed 62 rows containing missing values (`geom_point()`).

Wir kriegen zwei Warnungen. Auf der shape Seite können wir nur 6 verschiedene Symbole als Punkte darstellen, wir haben aber 7. Eventuell können wir hier manuell auf 7 kommen.

Zusätzlich werden 62 Zeilen vernachlässigt, da Missing Values vorliegen. Diese sind die SUVs, die nicht geplotted wurden, da mehr als 6 Formen nicht mögich sind.
Statt Farben unf Formen können wir auch Größe und Transparenz plotten.

# Left
ggplot(mpg, aes(x = displ, y = hwy, size = class)) +
  geom_point()
## Warning: Using size for a discrete variable is not advised.

# Right
ggplot(mpg, aes(x = displ, y = hwy, alpha = class)) +
  geom_point()
## Warning: Using alpha for a discrete variable is not advised.

Warnungen erscheinen ebenfalls. Es ist nicht ratsam eine nicht-ordinale diskrete Variable mit Hilfe einer geordneten aes (size oder alpha) abzubilden, da eine Reihenfolge hier gar nicht existiert. Größe und Transparenz suggerieren dies aber.
Bilden wir ein aes ab, so sorgt ggplot2 für den Rest. Eine Legende, die die Zuordnung zwischen den Stufen und Werten erklärt. Diese wird bei uns nicht gebaut, dafür Achsen mit Markierungen und Werten. Du kannst aes Eigenschaften unseres geom manuell setzen, wie z.B. blaue Punkte.

ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_point(color = "blue")

Ein aesthetic kannst du setzen durch den Namen als Argument der geom Funktion, als Wert musst du etwas sinnvolles finden:

  • Name der Farbe als String
  • Größe des Punktes in mm
  • Form des Punktes als Zahl (0-20), siehe Hilfeseiten.

Geometrische Objekte

# Left
ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_point()

# Right
ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_smooth()
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Beide Plots nutzen verschiedene geometrische Objekte (geom), um die Daten zu repräsentieren:

point geom (Punkte) vs. smooth geom (Linie).

Jede geom Funktion in ggplot2 nimmt ein mapping Argument, aber nicht jedes aes funktioniert mit jedem geom. Den shape einer Linie zu setzen macht keinen Sinn. Wenn du es versuchst, so ignoriert ggplot2 dieses aes mapping.

ggplot(mpg, aes(x = displ, y = hwy, shape = drv)) + 
  geom_smooth()
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

ggplot(mpg, aes(x = displ, y = hwy, linetype = drv)) + 
  geom_smooth()
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Hier sorgt geom_smooth() für drei Linie, basierend auf ihrem drv Wert (Antrieb). 4 steht für 4-Rad-Antrieb, f Vorderantrieb, r Heckantrieb.

ggplot(mpg, aes(x = displ, y = hwy, linetype = drv, color = drv)) + 
  geom_smooth() +
  geom_point()
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Dieser Plot enthält zwei geoms im selben Graphen. Viele geoms, wie geom_smooth() geben multiple Reihen von Daten aus. Für sie kannst du group aes zu kategorialen Variablen setzen, um multiple Objekte zu zeichnen. ggplot2 zeichnet aber immer ein separates Objekt für jeden Wert der gruppierenden Variable. Mach davon gebrauch, da der group aes keine Legende liefert.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_smooth()
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_smooth(aes(group = drv))
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_smooth(aes(color = drv), show.legend = FALSE)
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Wenn du Zuordnungen in einer geom Funktion platzierst, behandelt ggplot2 sie als lokale Mappings. Es erweitert oder überschreibt die globalen Mappings für diese Ebene (layer). So kann man verschiedene aes in verschiedenen Ebenen anzeigen.

ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_point(aes(color = class)) + 
  geom_smooth()
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Diese Idee kannst du genauso benutzen, um verschiedene Daten für jede Ebene zu benutzen. Rote Punkte und Kreise zeigen Zweisitzer-Autos an. Das lokale Daten-Argument in geom_smooth() überschreibt das globale in ggplot2 für diese eine Ebene.

ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_point() + 
  geom_point(
    data = mpg |> filter(class == "2seater"), 
    color = "red"
  ) +
  geom_point(
    data = mpg |> filter(class == "2seater"), 
    shape = "circle open", size = 3, color = "red"
  )

Geoms sind die wichtigen Fundamente bei der Erstellung von ggplots. Der Look des Plots kann komplett durch die Wahl dieser bestimmt und verändert werden. Unten erkennen wir, dass es zwei Ausreißer gibt, die Verteilung zweigipflig und rechtsschief ist. Dafür betrachten wir uns verschiedene Plots.

# Left
ggplot(mpg, aes(x = hwy)) +
  geom_histogram(binwidth = 2)

# Middle
ggplot(mpg, aes(x = hwy)) +
  geom_density()

# Right
ggplot(mpg, aes(x = hwy)) +
  geom_boxplot()

ggplot2 bietet mehr als 40 geoms an, es gibt aber natürlich noch mehr Möglichkeiten Link.

Die Dichte mithilfe von ridgeline plots zu erstellen ist sehr chic. fill und color machen die Dichten bunt und alpha transparent.

library(ggridges)
## Warning: Paket 'ggridges' wurde unter R Version 4.2.3 erstellt
ggplot(mpg, aes(x = hwy, y = drv, fill = drv, color = drv)) +
  geom_density_ridges(alpha = 0.5, show.legend = FALSE)
## Picking joint bandwidth of 1.28

#> Picking joint bandwidth of 1.28

Facets

Schon kennengelernt haben wir facet_wrap(), das Plots in Unterplots unterteilt, basierend auf einer kategorialen Variable.

ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_point() + 
  facet_wrap(~cyl)

Um deinen Plot weiter zu unterteilen auf zwei Variablen, benutze facet_grid().

ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_point() + 
  facet_grid(drv ~ cyl)

cyl und drv sind kategoriale Variablen. Für beide werden immer wieder dieselben Achsen x und y gewählt. Die Skala bleibt unverändert. Setzt du das scales Argument in der facet Funktion auf "free", so werden automatisch verschiedene Bereiche der Achsen gesetzt.
Weitere Arguemnte sind free_x und free_y für jeweils nur eine der beiden Achsen.

ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_point() + 
  facet_grid(drv ~ cyl, scales = "free")

Statistische Transformationen

Ein Balkendiagramm mit geom_bar() oder geom_col() ist schnell erstellt. Der cut unseres Diamantendatensatzes ist schnell visualisiert.

ggplot(diamonds, aes(x = cut)) + 
  geom_bar()

Auf der y-Achse wird “count” abgetragen. Diese ist aber keine Variable in diamonds. Der Algorithmus, der benutzt wird um neue Werte für jeden Graph zu berechnen, heißt stat (statistical transformation).

Welches stat ein geom benutzt, kannst du am default Wert sehen. ?geom_bar zeigt, dass der default Wert für statcount” ist. geom_bar() benutzt also stat_count(), welches auf derselben Seite wie geom_bar() dokumentiert ist. Im Abschnitt “Computed Variables” siehst du, dass zwei neue Variablen berechnet werden: “count” und “prop”. Jedes geom hat ein default stat und andersrum. Du brauchst dir also keine Sorgen über die stats zu machen, wenn du geoms benutzt. Es gibt jedoch drei Gründe, warum du ein stat explizit benutzen solltest:

  1. Weil du den default stat überschreiben willst. So können wir die Höhe der Balken über die y Variable der rohen Werte darstellen.
cut_frequencies <- tribble(
  ~cut,         ~freq,
  "Fair",       1610,
  "Good",       4906,
  "Very Good",  12082,
  "Premium",    13791,
  "Ideal",      21551
)

ggplot(cut_frequencies, aes(x = cut, y = freq)) +
  geom_bar(stat = "identity")

  1. Du willst den default überschreiben, da du z.B. an den relativen, statt den absoluten Häufigkeiten interessiert bist.
ggplot(diamonds, aes(x = cut, y = after_stat(prop), group = 1)) + 
  geom_bar()

  1. Du willst mehr Aufmerksamkeit auf die statistische Transformation legen. stat_summary() fasst die y-Werte zusammen für jeden x-Wert und gibt Median, Minimum und Maximum aus.
ggplot(diamonds) + 
  stat_summary(
    aes(x = cut, y = depth),
    fun.min = min,
    fun.max = max,
    fun = median
  )

ggplot2 bietet mehr als 20 stats an. Jedes ist eine Funkton.

Position Adjustments

Die Farben der Balken kannst du mit color oder fill gestalten.

ggplot(diamonds, aes(x = cut, color = cut)) + 
  geom_bar()

ggplot(diamonds, aes(x = cut, fill = cut)) + 
  geom_bar()

Was passiert jetzt, wenn du eine weitere Variable hinzufügst? Die Balken stapeln sich automatisch. Jedes Rechteck repräsentiert eine Kombination aus cut und clarity.

ggplot(diamonds, aes(x = cut, fill = clarity)) + 
  geom_bar()

Das Stapeln wird automatisch durch die position adjustment ausgeführt. Du kannst es auch verhindern, indem du position auf "identity", "dodge", oder "fill" setzt.

  1. position = "identity" platziert jedes Objekt genau an der anfallenden Position ohne es zu stapeln. Für Balken ist das natürlich nicht sehr sinnvoll, da viele Überlappungen entstehen. Wir können die Balken transparent machen (halb, ganz), um das Diagramm ein wenig anschaulicher zu gestalten. alpha und fill = NA helfen.
ggplot(diamonds, aes(x = cut, fill = clarity)) + 
  geom_bar(alpha = 1/5, position = "identity")

ggplot(diamonds, aes(x = cut, color = clarity)) + 
  geom_bar(fill = NA, position = "identity")

  1. position = "fill" arbeitet wie das Stapeln, nur dass jedes Set von Balken die gleiche Höhe hat. So lassen sich Anteile leichter vergleichen.
ggplot(diamonds, aes(x = cut, fill = clarity)) + 
  geom_bar(position = "fill")

  1. position = "dodge" platziert die überlappenden Objekte direkt nebeneinander. So lassen sich die individuellen Werte leichter vergleichen.
ggplot(diamonds, aes(x = cut, fill = clarity)) + 
  geom_bar(position = "dodge")

Ein weiteres nützliches Adjustment lässt sich für Scatterplots finden.

ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_point()

Hier haben wir 126 Punkte, obwohl 234 Beobachtungen im Datensatz existieren. Warum?
Weil sich viele Punkte überlappen. Das nennt man overplotting. Die Verteilung der Daten ist so schwerer zu erkennen. Das lässt sich durch position = "jitter" vermeiden. Die Punkte werden nahe beieinander platziert.

ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_point(position = "jitter")

Koordinatensytem

Das kartesische Koordinatensystem mit x-Achse und y-Achse ist bekannt. Es gibt aber noch zwei weitere Koordinatensysteme.

  1. coord_quickmap() setzt das Abbildungsverhältnis für Karten. Das ist bei Geodaten mit ggplot2 wichtig. Hier wird nichts verzerrt, die Proportionen bleiben maßstabsgetreu.
nz <- map_data("nz")

ggplot(nz, aes(x = long, y = lat, group = group)) +
  geom_polygon(fill = "white", color = "black")

ggplot(nz, aes(x = long, y = lat, group = group)) +
  geom_polygon(fill = "white", color = "black") +
  coord_quickmap()

  1. coord_polar() benutzt Polarkoordianten. Auch eine schöne Visualisierung (Coxcomb Chart).
bar <- ggplot(data = diamonds) + 
  geom_bar(
    mapping = aes(x = cut, fill = cut), 
    show.legend = FALSE,
    width = 1
  ) + 
  theme(aspect.ratio = 1) +
  labs(x = NULL, y = NULL)

bar + coord_flip()

bar + coord_polar()

Layered Grammar of Graphics

Ein Template für Grafiken mit Position Adjustments, stats, Koordinatensystem und Faceting sieht jetzt so aus.

ggplot(data = <DATA>) + 
  <GEOM_FUNCTION>(
     mapping = aes(<MAPPINGS>),
     stat = <STAT>, 
     position = <POSITION>
  ) +
  <COORDINATE_FUNCTION> +
  <FACET_FUNCTION>

Unser neues Template nimmt sieben Parameter. In der Praxis musst du natürlich selten alle sieben Parameter nennen für einen Graphen, da defaults vorhanden sind.

Am Anfang steht der Datensatz, den du so transformierst, dass er die Informationen enthält, die du brauchst (mit stat). Wähle ein geometrisches (geom) Objekt, um jede Beobachtung darzustellen. Benutze die aes, um Variablen darzustellen. Dann folgt das Mapping, du wählst ein passendes Koordinatensystem für deine geoms, wählst die Achsen weise.

Du hast einen Graphen, kannst Position Adjustments vornehmen, den Grapen splitten in Subplots. Mehrere Schichten können hinzugefügt werden, wobei jede Schicht einen Dataset, geom, Mappings, stat und Position Adjustment benutzt. Mit diesem Verfahren kannst du Hunderte von Plots bauen.

Visualisieren: Explorative Datenanalyse (EDA)

Einleitung

In diesem Kapitel werden Daten visualisiert, und transformiert. EDA ist ein wichtiger Teil der Datenanalyse. Data Cleaning gehört hier genauso zu, wie die Modelling.

Voraussetzungen

Alles was wir bisher mithilfe von dplyr und ggplot2 kennengelernt haben, kommt hier zur Anwendung.

library(tidyverse)

Fragen

Das Ziel der EDA ist es ein Verständnis deiner Daten zu entwickeln. Durch Fragestellungen wird dein Interesse auf einen bestimmten Bereich gelenkt, so dass diese dir helfen die passenden Graphiken, Modelle und Transformstionen zu wählen.
Es gibt nicht DIE passende Frage, aber Fragen über die Streuung in den Variablen und Kovarianz zwischen den Daten zu stellen, ist nie verkehrt.

Variation

Jede Messung stetiger oder diskreter Variablen bringt meist eine gewisse Streuung mit sich. Gewicht, Größe und Alter in der Bevölkerung sind nämlich nicht konstant. Jede Variable hat ihr eigenes Muster der Streuung. Um dieses zu verstehen, hilft es erst einmal es zu visualisieren.

Wir beginnen mit der Visualisierung von Gewichten (carat). Unser Diamantendatensatz dürfte mittlerweile bekannt sein. 54 000 Diamanten liegen vor und carat ist hierbei eine numerische Variable. Wir können sie somit im Histogramm darstellen.

ggplot(diamonds, aes(x = carat)) +
  geom_histogram(binwidth = 0.5)

Typische Werte

Im Balkendiagramm und Histogramm stehen hohe Balken für gewöhnliche und kurze Balken für seltenen Werte. Liegt kein Balken vor, so fehlen hier die Ausprägungen.

  • Welche Werte kommen warum am häufigsten vor?
  • Welche Werte sind rar und wieso?
  • Gibt es ein Muster in den Daten?

Bei den Diamanten fragt man sich, wieso es Brüche gibt und warum ganze Werte überproportional häufig vorkommen. Warum ist die Verteilung der Peaks rechtsschief?

smaller <- diamonds |> 
  filter(carat < 3)

ggplot(smaller, aes(x = carat)) +
  geom_histogram(binwidth = 0.01)

Die Länge von 272 Eruptionen eines Geysirs zeigt ein interessantes Muster auf.

ggplot(faithful, aes(x = eruptions)) + 
  geom_histogram(binwidth = 0.25)

Viele Fragen haben mit der Abhängigkeit von zwei Variablen zu tun. Eine erklärt das Verhalten der anderen.

Seltene Werte

Ausreißer sind unübliche Beobachtungen. Sie scheinen nicht in das Muster der Verteilung zu passen. Manchmal sind sie Eintragungsfehler, andere Male geben sie neue Erkenntnisse. Im Histogramm sind sie manchmal schwer zu entdecken.

ggplot(diamonds, aes(x = y)) + 
  geom_histogram(binwidth = 0.5)

Um das Entdecken leichter zu machen, müssen wir ein wenig in die Daten reinzoomen.

ggplot(diamonds, aes(x = y)) + 
  geom_histogram(binwidth = 0.5) +
  coord_cartesian(ylim = c(0, 50))

coord_cartesian() hat auch ein xlim() Argument, so dass du in den Bereich der x-Achse reinzoomen kannst. ggplot2 hat auch xlim() und ylim() Funktionen, die Daten außerhalb der Grenzen entsorgen.
So entdecken wir drei unübliche Werte: 0, ~30, ~60.

unusual <- diamonds |> 
  filter(y < 3 | y > 20) |> 
  select(price, x, y, z) |>
  arrange(y)
unusual
## # A tibble: 9 × 4
##   price     x     y     z
##   <int> <dbl> <dbl> <dbl>
## 1  5139  0      0    0   
## 2  6381  0      0    0   
## 3 12800  0      0    0   
## 4 15686  0      0    0   
## 5 18034  0      0    0   
## 6  2130  0      0    0   
## 7  2130  0      0    0   
## 8  2075  5.15  31.8  5.12
## 9 12210  8.09  58.9  8.06

Die y Variable misst einer der drei Dimensionen des Diamanten in mm. Eine Breite von 0 mm ist natürlich nicht möglich, also falsch. Die großen Diamanten müssen auch falsch sein, da die Kosten nicht mit der Größe korrelieren.
Die Analyse kann mit und ohne Ausreißer gefahren werden. Hier können sie rausgenommen werden. Verändern sie das Bild der Daten; so meist nicht. Welche Gründe gibt es? Ein Ausschluss muss gerechtfertigt sein.

Unübliche Werte bearbeiten

Bei der Analyse von unüblichen Werten hast du zwei Möglichkeiten.

  1. Schließe die ganze Zeile aus:
diamonds2 <- diamonds |> 
  filter(between(y, 3, 20))

Es ist nicht zu empfehlen, da ganze Beobachtungsreihen ausgeschlossen werden, statt einer defekten. Auch kann dann dein Datensatz zu stark reduziert sein.

  1. Ersetze die ungewöhnlichen Werte durch Missing Values. Dies geschieht am einfachsten mit mutate(). Der ifelse() Befehl hilft hier. if_else() ist nicht ifelse(). Da bei if_else() die erstellten Variablen von verschiedenem Datentyp sind, müssen wir die NA auch zum Datentyp double transformieren. Bei ifelse() ist dies nicht nötig.
diamonds2 <- diamonds |> 
  mutate(y = if_else(y < 3 | y > 20, NA_real_, y))

ifelse() hat drei Argumente. Das erste test sollte ein logischer Vektor sein. Alternativ benutze case_when().
Erstellst du einen Plot mit NA’s, so warnt dich ggplot2, dass sie entfernt wurden.

ggplot(diamonds2, aes(x = x, y = y)) + 
  geom_point()
## Warning: Removed 9 rows containing missing values (`geom_point()`).

#> Warning: Removed 9 rows containing missing values (`geom_point()`).

Diese Warnung kann mit na.rm = TRUE unterdrückt werden.

ggplot(diamonds2, aes(x = x, y = y)) + 
  geom_point(na.rm = TRUE)

In einem anderen Beispiel wollen wir herausfinden, inwiefern sich Beobachtugen unterscheiden, bei denen Werte vorliegen im Gegensatz zu Beobachtungen, bei denen Missing Values vorhanden sind. nycflights13::flights hat in dep_time fehlenden Werte, sodass der Flug wahrscheinlich gecancelt wurde. Vergleichen wir also die planmäßigen Abflugzeiten der Flüge bzw. abgesagten Flüge.

nycflights13::flights |>
  mutate(
    cancelled = is.na(dep_time),
    sched_hour = sched_dep_time %/% 100,
    sched_min = sched_dep_time %% 100,
    sched_dep_time = sched_hour + (sched_min / 60)    
  ) |>
  ggplot(aes(x = sched_dep_time)) + 
  geom_freqpoly(aes(color = cancelled), binwidth = 1/4)

Der Vergleich ist schwierig, da hier viel mehr Flüge als abgesagte sind.

Kovariation

Die Variation betrachtet eine Variable, die Kovariation beschreit das Verhalten zwischen Variablen. Die Beziehung zwischen zwei oder mehr Variablen zu visualisieren, hilft bei der Analyse natürlich enorm.

Kategoriale und numerische Variable

Zum Beispiel der Preis eines Diamanten wird mit der Qualität verglichen (cut).

ggplot(diamonds, aes(x = price)) + 
  geom_freqpoly(aes(color = cut), binwidth = 500, size = 0.75)
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.

#> Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
#> ℹ Please use `linewidth` instead.

Wieder nicht ideal, da es große Unterschiede in den Häufigkeiten gibt.

ggplot(diamonds, aes(x = cut)) + 
  geom_bar()

Um den Vergleich einfacher zu machen, werden die Werte auf der y-Achse ausgetauscht. Nehmen wir die Dichte, so ist die Fläche unter dem Graphen für alle cut Werte immer gleich eins.

ggplot(diamonds, aes(x = price, y = after_stat(density))) + 
  geom_freqpoly(aes(color = cut), binwidth = 500, size = 0.75)

Da density keine Variable des diamonds Datensatzes ist, müssen wir es berechnen. Dafür benutzen wir die after_stat() Funktion.
Das Ergebnis jedoch überrascht. Es sieht so aus, als ob die niedrigste (fair) Qualität, den höchsten Durchschnittspreis hat.

Ein visuell einfacher Plot, ist der Boxplot.

ggplot(diamonds, aes(x = cut, y = price)) +
  geom_boxplot()

Weniger Informationen sehen wir, aber dafür sind die Boxplots kompakter. Aber auch hier kann an der These festgehalten werden, dass qualitativ bessere Diamanten günstiger sind. Warum ist das wohl so?
cut ist hier ein geordneter Faktor. Viele Variablen haben nicht eine Anordnung, so dass sie neu sortiert werden müssen. Eine Möglichkeit ist hier die fct_reorder() Funktion.
Im mpg Datensatz z.B. wollen wir die highway mileage (Fahrleistung auf Autobahn) je nach class (type of car) darstellen.

ggplot(mpg, aes(x = class, y = hwy)) +
  geom_boxplot()

Sortieren wir class neu, basierend aud dem Median von hwy:

ggplot(mpg,
       aes(x = fct_reorder(class, hwy, median), y = hwy)) +
  geom_boxplot()

Oder:

ggplot(mpg,
       aes(x = hwy, y = fct_reorder(class, hwy, median))) +
  geom_boxplot()

Zwei kateroriale Variablen

Hier musst du für jede Kombination von Levels die Häufigkeiten zählen.

ggplot(diamonds, aes(x = cut, y = color)) +
  geom_count()

Die Größe der Kreise zeigt wie viele Beobachtungen in jedes Paar fallen.
Mit dplyr lassen sich die Counts bestimmen und mit geom_tile() visualisieren.

diamonds |> 
  count(color, cut) |>  
  ggplot(aes(x = color, y = cut)) +
  geom_tile(aes(fill = n))

Die Reihen und Spalten lassen sich wieder ordnen und eine Heatmap wird visualisiert.

Zwei numerische Variablen

Kovariation kannst du als Muster in den Punkten sehen. Eine exponentiale Beziehung zwischen Karatgröße und Preis von Diamanten z.B.

ggplot(diamonds, aes(x = carat, y = price)) +
  geom_point()

Bei großen Datensätzen werden Scatterplots jedoch unhandlich, da sich Punkte überlappen: der alpha aesthetic hilft.

ggplot(diamonds, aes(x = carat, y = price)) + 
  geom_point(alpha = 1 / 100)

Für große Datensätze kann transparency aber herausfordernd sein. Wir können aber die Daten gruppieren. Schon kennengelernt haben wir dafür geom_histogram() und geom_freqpoly(). Wir haben in eine Dimension gruppiert, jetzt in zwei Dimensionen (geom_bin2d() und geom_hex()).
Farben zeigen an, wieviele Punkte in jedes Rechteck fallen. Es wird zwischen Rechtecken und Sechsecken unterschieden.

ggplot(smaller, aes(x = carat, y = price)) +
  geom_bin2d()

# install.packages("hexbin")
ggplot(smaller, aes(x = carat, y = price)) +
  geom_hex()

Eine weitere, gute Möglichkeit ist es stetige als kategoriale Variablen zu gruppieren und dann entsprechende Grafiken zu wählen, wie Boxplots.

ggplot(smaller, aes(x = carat, y = price)) + 
  geom_boxplot(aes(group = cut_width(carat, 0.1)))

cut_width(x, width) unterteilt x in Gruppen der Breite width. Jedoch erkennen wir nicht wieviele Beobachtungen in jeden Boxplot fallen, sondern nur die Verteilung. Wir können jedoch die Breite proportional machen zu der Anzahl der Beobachtungen, mit varwidth = TRUE.
Ein anderer Ansatz ist es dieselbe Anzahl an Punkten in jeder Gruppe anzeigen zu lassen. Das macht cut_number():

ggplot(smaller, aes(x = carat, y = price)) + 
  geom_boxplot(aes(group = cut_number(carat, 20)))

Muster und Modelle

Muster in den Daten geben Hinweise über Zusammenhänge. Wenn ein Zusammenhang zwischen zwei Variablen existiert, so erscheint dieser auch als Muster in den Daten. Wenn du ein Muster in den Daten findest, frag dich:

  • Kann das Muster auf Zufall beruhen?
  • Wie kannst du den Zusammenhang über das Muster beschreiben?
  • Wie stark ist dieser?
  • Welche anderen Variablen beeinflussen den Zusammenhang?
  • Ändert sich der Zusammenhang, wenn du dir Untergruppen deiner Daten anschaust?

Das Streudiagramm der Eruptionslänge und Eruptionszeit von einem Geysir zeigt ein Muster auf: längere Wartezeit = längere Eruption. Zwei Cluster sind klar zu erkennen.

ggplot(faithful, aes(x = eruptions, y = waiting)) + 
  geom_point()

Modelle sind Werkzeuge, um Muster aus den Daten zu ziehen. Gucken wir uns den Diamantendatensatz an. Der Zusammenhang zwischen cut und price ist schwer zu verstehen, da cut und carat und carat und price miteinander korrelieren. Wir können aber über ein Model die starke Beziehung zwischen Preis und Karat entfernen.

library(tidymodels)
## Warning: Paket 'tidymodels' wurde unter R Version 4.2.3 erstellt
## ── Attaching packages ────────────────────────────────────── tidymodels 1.0.0 ──
## ✔ broom        1.0.4     ✔ rsample      1.1.1
## ✔ dials        1.1.0     ✔ tune         1.0.1
## ✔ infer        1.0.4     ✔ workflows    1.1.3
## ✔ modeldata    1.1.0     ✔ workflowsets 1.0.0
## ✔ parsnip      1.0.4     ✔ yardstick    1.1.0
## ✔ recipes      1.0.5
## Warning: Paket 'broom' wurde unter R Version 4.2.3 erstellt
## Warning: Paket 'parsnip' wurde unter R Version 4.2.3 erstellt
## Warning: Paket 'recipes' wurde unter R Version 4.2.3 erstellt
## Warning: Paket 'rsample' wurde unter R Version 4.2.3 erstellt
## Warning: Paket 'workflows' wurde unter R Version 4.2.3 erstellt
## ── Conflicts ───────────────────────────────────────── tidymodels_conflicts() ──
## ✖ scales::discard() masks purrr::discard()
## ✖ dplyr::filter()   masks stats::filter()
## ✖ recipes::fixed()  masks stringr::fixed()
## ✖ dplyr::lag()      masks stats::lag()
## ✖ yardstick::spec() masks readr::spec()
## ✖ recipes::step()   masks stats::step()
## • Learn how to get started at https://www.tidymodels.org/start/
diamonds <- diamonds |>
  mutate(
    log_price = log(price),
    log_carat = log(carat)
  )

diamonds_fit <- linear_reg() |>
  fit(log_price ~ log_carat, data = diamonds)

diamonds_aug <- augment(diamonds_fit, new_data = diamonds) |>
  mutate(.resid = exp(.resid))

ggplot(diamonds_aug, aes(x = carat, y = .resid)) + 
  geom_point()

Jetzt können wir die Beziehng zwischen cut und price herstellen.

ggplot(diamonds_aug, aes(x = cut, y = .resid)) + 
  geom_boxplot()

Ein weiteres Beispiel:

x <- 1:100 
x
##   [1]   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18
##  [19]  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36
##  [37]  37  38  39  40  41  42  43  44  45  46  47  48  49  50  51  52  53  54
##  [55]  55  56  57  58  59  60  61  62  63  64  65  66  67  68  69  70  71  72
##  [73]  73  74  75  76  77  78  79  80  81  82  83  84  85  86  87  88  89  90
##  [91]  91  92  93  94  95  96  97  98  99 100
z <- c(rep(5, 20), rep(10, 20), rep(-1, 20), rep(-5, 20), rep(-2,20))
y <- rnorm(100) + 1:100 + z
y
##   [1]  7.015990  7.135769  6.606436  7.840337  9.869030  9.618894 12.810674
##   [8] 13.775878 13.579231 12.957643 14.051958 17.205347 17.981682 18.623830
##  [15] 20.934486 20.475597 21.964913 23.336387 25.750038 24.233427 30.526972
##  [22] 32.611022 32.796767 34.281078 35.146259 35.949592 35.969469 38.230510
##  [29] 38.094787 40.179607 40.275137 42.516568 42.639685 42.212677 43.529826
##  [36] 46.675837 46.988548 47.387330 49.720200 49.414193 39.653590 41.377265
##  [43] 42.738059 43.589884 42.833508 43.255760 47.537304 46.061830 47.246834
##  [50] 49.703073 49.666757 53.082091 50.887210 53.634193 52.820972 56.265948
##  [57] 56.967526 57.387959 57.284937 60.442728 54.872951 57.611213 59.036282
##  [64] 60.152462 60.274053 62.420894 62.070902 64.550844 64.345192 64.398109
##  [71] 66.918722 66.013398 68.573897 68.626073 70.314075 69.603448 70.436875
##  [78] 72.740202 72.968661 78.116884 77.278723 79.791944 80.966527 82.189356
##  [85] 82.753201 83.365858 84.618339 85.702666 86.488206 86.013509 88.622054
##  [92] 91.093840 91.155833 92.823194 93.622326 94.741770 94.137989 96.263657
##  [99] 98.484129 98.466293
kat <- c(rep("blau", 20), rep("grün", 20), rep("orange", 20), rep("rot", 20), rep("schwarz", 20))
df <- tibble(x, y, kat)
df
## # A tibble: 100 × 3
##        x     y kat  
##    <int> <dbl> <chr>
##  1     1  7.02 blau 
##  2     2  7.14 blau 
##  3     3  6.61 blau 
##  4     4  7.84 blau 
##  5     5  9.87 blau 
##  6     6  9.62 blau 
##  7     7 12.8  blau 
##  8     8 13.8  blau 
##  9     9 13.6  blau 
## 10    10 13.0  blau 
## # ℹ 90 more rows
df |>
  ggplot(aes(x=x,y=y)) +
  geom_point()

d_fit <- linear_reg() |>
  fit(y ~ x, data = df)
  
d_a <- augment(d_fit, new_data = df) |>
mutate(.resid = 1 * .resid)
d_a
## # A tibble: 100 × 5
##        x     y kat   .pred .resid
##    <int> <dbl> <chr> <dbl>  <dbl>
##  1     1  7.02 blau   9.04  -2.03
##  2     2  7.14 blau   9.91  -2.77
##  3     3  6.61 blau  10.8   -4.16
##  4     4  7.84 blau  11.6   -3.80
##  5     5  9.87 blau  12.5   -2.63
##  6     6  9.62 blau  13.4   -3.75
##  7     7 12.8  blau  14.2   -1.42
##  8     8 13.8  blau  15.1   -1.32
##  9     9 13.6  blau  16.0   -2.38
## 10    10 13.0  blau  16.8   -3.87
## # ℹ 90 more rows
ggplot(d_a, aes(x = x, y = .resid)) + 
  geom_point() +
geom_vline(xintercept = c(20,40,60,80), lty = 4) 

ggplot(d_a, aes(x = kat, y = .resid)) +
geom_boxplot()

Visualisieren: Kommunikation

Einleitung

In jeder tieferen Analyse werden Massen an Plots erstellt, von denen die meisten schnell wieder verworfen werden. Hast du deine Schlüsse aus den Daten gezogen, gilt es fast immer sie zu kommunizieren. Das Problem ist oft, dass dein Publikum nicht so tief in der Analyse, in den Daten, in der Fachrichtung, drin steckt wie du. So muss es dein Ziel sein, deine Plots so selbsterklärend wie möglich zu gestalten. In diesem Abschitt lernst du ein paar Werkzeuge von ggplot2 kennen, die dir dabei helfen.

Voraussetzungen

Wieder konzentrieren wir uns hier auf ggplot2 und die Pakete ggrepel, patchwork und dplyr.

library(ggrepel)
## Warning: Paket 'ggrepel' wurde unter R Version 4.2.3 erstellt
library(patchwork)
## Warning: Paket 'patchwork' wurde unter R Version 4.2.3 erstellt

Labels

Etiketten bzw. Labels helfen natürlich immer eine Grafik anschaulich zu machen. Sie können mithilfe der labs() Funktion hinzugefügt werden.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class)) +
  geom_smooth(se = FALSE) +
  labs(title = "Fuel efficiency generally decreases with engine size")
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Die Absicht ist es, die Haupterkenntnis zusammenzufassen. Nur den Plot zu beschreiben sollte man vermeiden. Zwei Möglichkeiten gibt es etwas hinzuzufügen:

  • subtitle fügt ein weiteres Detail in kleinerer Schrift darunter hinzu.

  • caption fügt Text unterhalb der Grafik rechts vom Plot hinzu, meist die Quelle der Daten.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class)) +
  geom_smooth(se = FALSE) +
  labs(
    title = "Fuel efficiency generally decreases with engine size",
    subtitle = "Two seaters (sports cars) are an exception because of their light weight",
    caption = "Data from fueleconomy.gov"
  )
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

labs() kannst du auch benutzen, um Achsen- und Legenden-Titel zu ersetzen.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class)) +
  geom_smooth(se = FALSE) +
  labs(
    x = "Engine displacement (L)",
    y = "Highway fuel economy (mpg)",
    color = "Car type"
  )
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Mathematische Gleichungen statt Strings können genutzt werden. Tausche hierfür "" aus durch quote() und schau dir die Optionen in der Hilfe ?plotmath an:

df <- tibble(
  x = 1:10,
  y = x ^ 2
)

ggplot(df, aes(x, y)) +
  geom_point() +
  labs(
    x = quote(sum(x[i] ^ 2, i == 1, n)),
    y = quote(alpha + beta + frac(delta, theta))
  )

Annotationen

Manchmal ist es sinnvoll auch individuelle Beobachtungen oder Gruppen von Beobachtungen zu labeln. geom_text() ist dabei ähnlich wie geom_point(), aber es hat einen zusätzlichen aes: label. Der macht es möglich Text (Labels) zu deinen Plots hinzuzufügen.
Es gibt zwei mögliche Quellen von Labels. Als erstes könntest du ein tibble haben, dass Labels anbietet. Im Folgenden schauen wir uns die Autos mit den größten Hubraum pro Antriebsart an und speichern die Informationen als neuen Data Frame label_info. Neue dplyr Funktionen helfen dabei.

label_info <- mpg |>
  group_by(drv) |>
  arrange(desc(displ)) |>
  slice_head(n = 1) |>
  mutate(
    drive_type = case_when(
      drv == "f" ~ "front-wheel drive",
      drv == "r" ~ "rear-wheel drive",
      drv == "4" ~ "4-wheel drive"
    )
  ) |>
  select(displ, hwy, drv, drive_type)

label_info
## # A tibble: 3 × 4
## # Groups:   drv [3]
##   displ   hwy drv   drive_type       
##   <dbl> <int> <chr> <chr>            
## 1   6.5    17 4     4-wheel drive    
## 2   5.3    25 f     front-wheel drive
## 3   7      24 r     rear-wheel drive

Diesen neuen Datensatz benutzen wir um die Labels direkt über den Plots anzuzeigen. Mit fontface und size können wir den Look der Labels direkt anpassen. Die Ausrichtung der Labels erfolgt mit hjust (“left”, “center”, “right”) und vjust (“top”, “center”, “botom”). theme(legend.position = "none") unterdrückt die Legende. Sie brauchen wir auf der rechten Seite jetzt natürlich nicht mehr.

ggplot(mpg, aes(x = displ, y = hwy, color = drv)) +
  geom_point(alpha = 0.3) +
  geom_smooth(se = FALSE) +
  geom_text(
    data = label_info, 
    aes(x = displ, y = hwy, label = drive_type),
    fontface = "bold", size = 5, hjust = "right", vjust = "bottom"
  ) +
  theme(legend.position = "none")
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Durch die Überlappungen ist der beschriftete Plot nicht mehr ganz so gut zu lesen. Ein Rechteck wird hinter den Text geworfen. geom_label() hilft. nudge_y hebt die Labels leicht vor die Punkte des Plots:

ggplot(mpg, aes(x = displ, y = hwy, color = drv)) +
  geom_point(alpha = 0.3) +
  geom_smooth(se = FALSE) +
  geom_label(
    data = label_info, 
    aes(x = displ, y = hwy, label = drive_type),
    fontface = "bold", size = 5, hjust = "right", alpha = 0.5, nudge_y = 2,
  ) +
  theme(legend.position = "none")
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Optimal ist es nicht. geom_label_repel() vom ggrepel Paket sorgt automatisch für eine Platzierung der Labels, sodass sie nicht überlappen.

ggplot(mpg, aes(x = displ, y = hwy, color = drv)) +
  geom_point(alpha = 0.3) +
  geom_smooth(se = FALSE) +
  geom_label_repel(
    data = label_info, 
    aes(x = displ, y = hwy, label = drive_type),
    fontface = "bold", size = 5, nudge_y = 2,
  ) +
  theme(legend.position = "none")
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Auch können wir gewisse Punkte optisch hervorheben mit geom_text_repel(). Hier fügen wir sogar zwei Schichten von Punkten übereinander, um die Punkte hervorzuheben.

potential_outliers <- mpg |>
  filter(hwy > 40 | (hwy > 20 & displ > 5))
  
ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  geom_text_repel(data = potential_outliers, aes(label = model)) +
  geom_point(data = potential_outliers, color = "red") +
  geom_point(data = potential_outliers, color = "red", size = 3, shape = "circle open")

Ein Label zum Plot kannst du über einen neuen Data Frame hinzufügen. Bestimme dazu die maximalen Werte von x und y und speichere diese Koordinaten. Dann setze die Beschriftung dort ein.

label_info <- mpg |>
  summarize(
    displ = max(displ),
    hwy = max(hwy),
    label = "Increasing engine size is \nrelated to decreasing fuel economy."
  )

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  geom_text(
    data = label_info, aes(label = label), 
    vjust = "top", hjust = "right"
  )

Direkt in die Ecken können wir den Text setzen über +Inf und -Inf. Da wir die exakten Positionen nicht brauchen, ertellen wir den Data Frame mithilfe von tibble().

label_info <- tibble(
  displ = Inf,
  hwy = Inf,
  label = "Increasing engine size is \nrelated to decreasing fuel economy."
)

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  geom_text(data = label_info, aes(label = label), vjust = "top", hjust = "right")

Ohne einen Data Frame funktioniert das Ganze natürlich auch. annotate() fügt ein geom zu einem Plot hinzu.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  annotate(
    geom = "text", x = Inf, y = Inf,
    label = "Increasing engine size is \nrelated to decreasing fuel economy.",
    vjust = "top", hjust = "right"
  )

Wir können ein Label-geom statt eines Text-geoms benutzen. Ein Segment-geom mit dem arrow Argument zieht die Aufmerksamkeit auf sich. x und y aes definieren den Startpunkt, xend und yend den Endpunkt.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  annotate(
    geom = "label", x = 3.5, y = 38,
    label = "Increasing engine size is \nrelated to decreasing fuel economy.",
    hjust = "left", color = "red"
  ) +
  annotate(
    geom = "segment",
    x = 3, y = 35, xend = 5, yend = 25, color = "red",
    arrow = arrow(type = "closed")
  )

"\n" überführt das Label in eine weitere Zeile. stringr::str_wrap() tut dies automatisch, indem man ihm die Anzahl der Zeichen vorgibt, pro Zeile.

"Increasing engine size is related to decreasing fuel economy." |>
  str_wrap(width = 40) |>
  writeLines()
## Increasing engine size is related to
## decreasing fuel economy.

Skalen

Eine weitere Möglichkeit deinen Plot besser für die Kommunikation zu machen ist die Anpassung der Skalen.

Default Skalen

ggplot2 setzt normalerweise die Skalen automatisch für dich. Wenn du tippst:

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class))

Dann fügt ggplot2 automatisch (hinter dem Vorhang) default Skalen hinzu.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class)) +
  scale_x_continuous() +
  scale_y_continuous() +
  scale_color_discrete()

Die Namensgebung läuft wie folgt ab: scale_ folgt der Names des aes, dann folgt _, dann der Name der Skala. Die default Skalen sind nach dem Typ der Variablen benannt: continuous, discrete, datetime, date. Es gibt jedoch Gründe die defaults zu überschreiben:

  • du willst die Parameter der default Skala optimieren. Du kannst die breaks der Achsen wechseln, oder die Labels der Legende.

  • du willst die Skala komplett ersetzen, und einen anderen Algorithmus verwenden.

Axis ticks und legend keys

Es gibt zwei Argumente, die das Erscheinen der Ticks auf der Achse und der keys auf der Legende beeinflussen: breaks und labels. Breaks kontrollieren die Position der Ticks. Labels kontrollieren die text labels. Die gewöhnlichste Anwendung von breaks ist das Überschreiben des default:

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  scale_y_continuous(breaks = seq(15, 40, by = 5))

labels kannst du genauso benutzen (Character Vektor derselben Länge wie breaks), du kannst aber auch ihn auf NULL setzen, um die Labels zu unterdrücken.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point() +
  scale_x_continuous(labels = NULL) +
  scale_y_continuous(labels = NULL)

Das label Argument kannst du mit der labelling Funktion vom scales Paket paaren, sodass die Formatierung von Zahlen wie Währungen und Prozenten leichter fällt. Links wird ein Dollarzeichen gesetzt, rechts durch 1000 geteilt und ein “K” für 1000 angefügt. Die Breaks werden auch individuell gesetzt.

# Left
ggplot(diamonds, aes(x = cut, y = price)) +
  geom_boxplot(alpha = 0.05) +
  scale_y_continuous(labels = scales::label_dollar())

# Right
ggplot(diamonds, aes(x = cut, y = price)) +
  geom_boxplot(alpha = 0.05) +
  scale_y_continuous(
    labels = scales::label_dollar(scale = 1/1000, suffix = "K"), 
    breaks = seq(1000, 19000, by = 6000)
  )

Eine weitere nützliche label Funktion ist label_percent():

ggplot(diamonds, aes(x = cut, fill = clarity)) +
  geom_bar(position = "fill") +
  scale_y_continuous(
    name = "Percentage", 
    labels = scales::label_percent()
  )

Hast du wenig Datenpunkte und willst hervorheben, wo die Beobachtung exakt angefallen ist, benutze breaks.

presidential |>
  mutate(id = 33 + row_number()) |>
  ggplot(aes(x = start, y = id)) +
  geom_point() +
  geom_segment(aes(xend = end, yend = id)) +
  scale_x_date(name = NULL, breaks = presidential$start, date_labels = "'%y")

Layout der Legende

Um die Achsen zu optimieren, nutze meist breaks und labels. Um die Legenden zu kontrollieren, benutze theme(). Sie kontrollieren den Nicht-Daten-Bereich. legend.position kontrolliert wo genau die Legende gezeichnet wird.

base <- ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class))

base + theme(legend.position = "left")

base + theme(legend.position = "top")

base + theme(legend.position = "bottom")

base + theme(legend.position = "right") # the default

Mit legend.position = "none" kannst du die Legende unterdrücken.
Kontrolliere die Anzahl an Zeilen und die Größe der angezeigten Punkte in der Legende mithilfe von guides().

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class)) +
  geom_smooth(se = FALSE) +
  theme(legend.position = "bottom") +
  guides(color = guide_legend(nrow = 1, override.aes = list(size = 4)))
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Ersetzen einer Skala

Oft ist es sinnvoll Transformationen deiner Variable zu plotten. Z.B. lässt sich so der Zusammenhang zwischen carat und price besser sehen.

# Left
ggplot(diamonds, aes(x = carat, y = price)) +
  geom_bin2d()

# Right
ggplot(diamonds, aes(x = log10(carat), y = log10(price))) +
  geom_bin2d()

Der Nachteil ist natürlich, dass die Achsen mit den transformierten Werten gelabelt sind. Das macht die Interpretation des Plots schwierig. Man muss jetzt aber schon genau auf die Achsen schauen.

ggplot(diamonds, aes(x = carat, y = price)) +
  geom_bin2d() + 
  scale_x_log10() + 
  scale_y_log10()

Die Farbpalette kann für Menschen mit Farbenblindheit angepasst werden.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = drv))

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = drv)) +
  scale_color_brewer(palette = "Set1")

Ein shape mapping hilft hier natürlich deutlich mehr.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = drv, shape = drv)) +
  scale_color_brewer(palette = "Set1")

Wenn eine vordefinierte Zuordnung zwischen Werten und Farben existiert, benutze scale_color_manual(). Die US Präsidenten können wir so optisch nach Parteizugehörigkeit hervorheben.

presidential |>
  mutate(id = 33 + row_number()) |>
  ggplot(aes(x = start, y = id, color = party)) +
  geom_point() +
  geom_segment(aes(xend = end, yend = id)) +
  scale_color_manual(values = c(Republican = "red", Democratic = "blue"))

Für stetige Farbeverläufe benutze scale_color_gradient() oder scale_fill_gradient(). Für divergierende Skalen benutze scale_color_gradient2(). So können die negativen und positiven Werte verschiedenen Farben übergeben werden.

Zooming

Es gibt drei Möglichkeiten die Plot Limits zu kontrollieren:

  1. Anpassen, welche Daten geplottet werden.
  2. Limits in jeder Skala setzen.
  3. xlim und ylim im coord_cartesian() setzen.

In eine Region zoomt man am besten über coord_cartesian():

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class)) +
  geom_smooth() +
  coord_cartesian(xlim = c(5, 7), ylim = c(10, 30))
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

mpg |>
  filter(displ >= 5, displ <= 7, hwy >= 10, hwy <= 30) |>
  ggplot(aes(x = displ, y = hwy)) +
  geom_point(aes(color = class)) +
  geom_smooth()
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Die Limits von Plots zu reduzieren ist meist äquivalent zu Subsetting. Es ist aber nicht selten sinnvoll die Limits zu erweitern, sodass zwei oder mehrere Plots besser miteinander verglichen werden können.

suv <- mpg |> filter(class == "suv")
compact <- mpg |> filter(class == "compact")

ggplot(suv, aes(x = displ, y = hwy, color = drv)) +
  geom_point()

ggplot(compact, aes(x = displ, y = hwy, color = drv)) +
  geom_point()

Um das Problem zu beheben, können wir den ganzen Datensatz auf die limits trainieren.

x_scale <- scale_x_continuous(limits = range(mpg$displ))
y_scale <- scale_y_continuous(limits = range(mpg$hwy))
col_scale <- scale_color_discrete(limits = unique(mpg$drv))

ggplot(suv, aes(x = displ, y = hwy, color = drv)) +
  geom_point() +
  x_scale +
  y_scale +
  col_scale

ggplot(compact, aes(x = displ, y = hwy, color = drv)) +
  geom_point() +
  x_scale +
  y_scale +
  col_scale

Themes

Du kannst die Nicht-Datenelemente deines Plots mit einem Thema/Theme individuell gestalten.

ggplot(mpg, aes(x = displ, y = hwy)) +
  geom_point(aes(color = class)) +
  geom_smooth(se = FALSE) +
  theme_bw()
## `geom_smooth()` using method = 'loess' and formula = 'y ~ x'

Acht Themes werden von ggplot2 per Default mitgeliefert. Auch das Paket ggthemes und ähnliche helfen weiter. Mehr dazu über die Hilfeseiten.

Layout

Das Patchwork Paket erlaubt es uns mehrere separate Plots in derselben Graphik zu vereinen. Die erstellten Plots musst du erst als Objekt speichern und kannst sie dann über + zusammenfügen.

p1 <- ggplot(mpg, aes(x = displ, y = hwy)) + 
  geom_point() + 
  labs(title = "Plot 1")
p2 <- ggplot(mpg, aes(x = drv, y = hwy)) + 
  geom_boxplot() + 
  labs(title = "Plot 2")
p1 + p2

Das Paket hat eine neue Funktionalität dem + Operator zugefügt. | platziert p1 und p3 nebeneinander und / schiebt p2 in die nächste Zeile.

p3 <- ggplot(mpg, aes(x = cty, y = hwy)) + 
  geom_point() + 
  labs(title = "Plot 3")
(p1 | p3) / p2

Patchwork erlaubt es uns die Legenden von mehreren Plots zu bündeln. Die Platzierung der Legende kann angepasst werden, die Dimensionen der Plots, ein gemeinsamer Titel, Untertitel, Überschrift, etc.
Wir haben 5 Plots jetzt, die Legenden wurden unterdrückt und die Legende oben erstellt mit & theme(legend.position = "top"). Die Höhe der Plots wurde angepasst.

p1 <- ggplot(mpg, aes(x = drv, y = cty, color = drv)) + 
  geom_boxplot(show.legend = FALSE) + 
  labs(title = "Plot 1")

p2 <- ggplot(mpg, aes(x = drv, y = hwy, color = drv)) + 
  geom_boxplot(show.legend = FALSE) + 
  labs(title = "Plot 2")

p3 <- ggplot(mpg, aes(x = cty, color = drv, fill = drv)) + 
  geom_density(alpha = 0.5) + 
  labs(title = "Plot 3")

p4 <- ggplot(mpg, aes(x = hwy, color = drv, fill = drv)) + 
  geom_density(alpha = 0.5) + 
  labs(title = "Plot 4")

p5 <- ggplot(mpg, aes(x = cty, y = hwy, color = drv)) + 
  geom_point(show.legend = FALSE) + 
  facet_wrap(~drv) +
  labs(title = "Plot 5")

(guide_area() / (p1 + p2) / (p3 + p4) / p5) +
  plot_annotation(
    title = "City and highway mileage for cars with different drive trains",
    caption = "Source: Source: https://fueleconomy.gov."
  ) +
  plot_layout(
    guides = "collect",
    heights = c(1, 3, 2, 4)
    ) &
  theme(legend.position = "bottom")

Mehr dazu über die Hilfe oder link.

Transformieren: Logical Vectors

Einleitung

In diesem Abschnitt lernen wir Werkzeuge für logische Vektoren kennen. Sie können nur TRUE, FALSE, oder NA annehmen. In deiner Analyse findest du sie recht selten, doch trotzdem werden sie fast immer kreiert und manipuliert.

Voraussetzungen

Die meisten Funktionen, die wir brauchen, werden natürlich schon von Base R bereitgestellt. Um Data Frames zu bearbeiten und für Datenbeispiele, laden wir aber dennoch:

library(tidyverse)
library(nycflights13)

Vergleiche

Ein gewöhnlicher Weg einen logischen Vektor zu erzeugen, ist es numerische Vergleiche mithilfe von <, <=, >, >=, != und == durchzuführen.
Bisher haben wir logische Variablen erzeugt mithilfe von filter(). Sie wurden berechnet, benutzt und dann wieder entsorgt. Im folgenden Beispiel finden sich über den Filter alle Abflüge am Tag, die ungefähr pünktlich waren.

flights |> 
  filter(dep_time > 600 & dep_time < 2000 & abs(arr_delay) < 20)
## # A tibble: 172,286 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      601            600         1      844            850
##  2  2013     1     1      602            610        -8      812            820
##  3  2013     1     1      602            605        -3      821            805
##  4  2013     1     1      606            610        -4      858            910
##  5  2013     1     1      606            610        -4      837            845
##  6  2013     1     1      607            607         0      858            915
##  7  2013     1     1      611            600        11      945            931
##  8  2013     1     1      613            610         3      925            921
##  9  2013     1     1      615            615         0      833            842
## 10  2013     1     1      622            630        -8     1017           1014
## # ℹ 172,276 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Die logischen Variablen sind hier nicht sichtbar, wir können sie aber sichtbar machen mit mutate():

flights |> 
  mutate(
    daytime = dep_time > 600 & dep_time < 2000,
    approx_ontime = abs(arr_delay) < 20,
    .keep = "used"
  )
## # A tibble: 336,776 × 4
##    dep_time arr_delay daytime approx_ontime
##       <int>     <dbl> <lgl>   <lgl>        
##  1      517        11 FALSE   TRUE         
##  2      533        20 FALSE   FALSE        
##  3      542        33 FALSE   FALSE        
##  4      544       -18 FALSE   TRUE         
##  5      554       -25 FALSE   FALSE        
##  6      554        12 FALSE   TRUE         
##  7      555        19 FALSE   TRUE         
##  8      557       -14 FALSE   TRUE         
##  9      557        -8 FALSE   TRUE         
## 10      558         8 FALSE   TRUE         
## # ℹ 336,766 more rows

So können wir den Code besser verstehen und überprüfen, ob jeder Schritt korrekt ausgeführt wurde. Der Filter sieht dann so aus:

flights |> 
  mutate(
    daytime = dep_time > 600 & dep_time < 2000,
    approx_ontime = abs(arr_delay) < 20,
  ) |> 
  filter(daytime & approx_ontime)
## # A tibble: 172,286 × 21
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      601            600         1      844            850
##  2  2013     1     1      602            610        -8      812            820
##  3  2013     1     1      602            605        -3      821            805
##  4  2013     1     1      606            610        -4      858            910
##  5  2013     1     1      606            610        -4      837            845
##  6  2013     1     1      607            607         0      858            915
##  7  2013     1     1      611            600        11      945            931
##  8  2013     1     1      613            610         3      925            921
##  9  2013     1     1      615            615         0      833            842
## 10  2013     1     1      622            630        -8     1017           1014
## # ℹ 172,276 more rows
## # ℹ 13 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>, daytime <lgl>,
## #   approx_ontime <lgl>

Punktvergleiche

Punktvergleiche mit == können sehr gefährlich sein, da sie ein falsches Ergebnis ausgeben können.

x <- c(1 / 49 * 49, sqrt(2) ^ 2)
x
## [1] 1 2
x == c(1, 2)
## [1] FALSE FALSE

Es wird von einer festen Anzahl an Nachkommastellen ausgegangen. Bei Wurzel aber auch mal gerundet.

print(x, digits = 16)
## [1] 0.9999999999999999 2.0000000000000004

Eine Option ist es dplyr::near() zu nutzen, dass geringfügige Abweichungen ignoriert.

near(x, c(1, 2))
## [1] TRUE TRUE

Missing Values

Fast jede Operation, die einen unbekannten Wert involviert, ist wieder unbekannt.

NA > 5
10 == NA
NA == NA
# NA

Willst du z.B. alle unbekannte Flüge finden, bei denen dept_time fehlt, so funktioniert dept_time == NA nicht, da das Ergebnis immer NA ist und filter() sortiert automatisch fehlende Werte aus.

flights |> 
  filter(dep_time == NA)

is.na()

is.na() funktioniert mit jedem Typ Vektor und gibt TRUE aus für Missing Values und FALSE für alles andere.

is.na(c(TRUE, NA, FALSE))
## [1] FALSE  TRUE FALSE
is.na(c(1, NA, 3))
## [1] FALSE  TRUE FALSE
is.na(c("a", NA, "b"))
## [1] FALSE  TRUE FALSE

Jetzt können wir uns alle Zeilen ausgeben lassen, bei denen in dept_time Missing Values vorkommen.

flights |> 
  filter(is.na(dep_time))
## # A tibble: 8,255 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1       NA           1630        NA       NA           1815
##  2  2013     1     1       NA           1935        NA       NA           2240
##  3  2013     1     1       NA           1500        NA       NA           1825
##  4  2013     1     1       NA            600        NA       NA            901
##  5  2013     1     2       NA           1540        NA       NA           1747
##  6  2013     1     2       NA           1620        NA       NA           1746
##  7  2013     1     2       NA           1355        NA       NA           1459
##  8  2013     1     2       NA           1420        NA       NA           1644
##  9  2013     1     2       NA           1321        NA       NA           1536
## 10  2013     1     2       NA           1545        NA       NA           1910
## # ℹ 8,245 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

In arrange() werden alle Missing Values ans Ende gesetzt. Das kannst du überschreiben, indem du nach is.na() sotierst.

flights |> 
  filter(month == 1, day == 1) |> 
  arrange(dep_time)
## # A tibble: 842 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 832 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>
flights |> 
  filter(month == 1, day == 1) |> 
  arrange(desc(is.na(dep_time)), dep_time)
## # A tibble: 842 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1       NA           1630        NA       NA           1815
##  2  2013     1     1       NA           1935        NA       NA           2240
##  3  2013     1     1       NA           1500        NA       NA           1825
##  4  2013     1     1       NA            600        NA       NA            901
##  5  2013     1     1      517            515         2      830            819
##  6  2013     1     1      533            529         4      850            830
##  7  2013     1     1      542            540         2      923            850
##  8  2013     1     1      544            545        -1     1004           1022
##  9  2013     1     1      554            600        -6      812            837
## 10  2013     1     1      554            558        -4      740            728
## # ℹ 832 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Boolsche Algebra

Hast du einmal alle logical vectors, so kannst du sie kombinieren: & ist “and”, | ist “oder”, ! ist “nicht” und xor() ist “exklusives oder”, also nicht die Schnittmenge.

Missing Values

df <- tibble(x = c(TRUE, FALSE, NA))

df |> 
  mutate(
    and = x & NA,
    or = x | NA
  )
## # A tibble: 3 × 3
##   x     and   or   
##   <lgl> <lgl> <lgl>
## 1 TRUE  NA    TRUE 
## 2 FALSE FALSE NA   
## 3 NA    NA    NA

Ein NA in einem logischen Vektor bedeutet entweder TRUE oder FALSE. TRUE | TRUE und FALSE | TRUE sind beide TRUE, also muss NA | TRUE auch TRUE sein. Ähnliches mit NA & FALSE.

Vermeide (x == a | b)

flights |> 
   filter(month == 11 | month == 12)
## # A tibble: 55,403 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013    11     1        5           2359         6      352            345
##  2  2013    11     1       35           2250       105      123           2356
##  3  2013    11     1      455            500        -5      641            651
##  4  2013    11     1      539            545        -6      856            827
##  5  2013    11     1      542            545        -3      831            855
##  6  2013    11     1      549            600       -11      912            923
##  7  2013    11     1      550            600       -10      705            659
##  8  2013    11     1      554            600        -6      659            701
##  9  2013    11     1      554            600        -6      826            827
## 10  2013    11     1      554            600        -6      749            751
## # ℹ 55,393 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

funktioniert. Wenn du aber auf der rechten Seite eine Zahl benutzt, so ist das wie ein TRUE.

flights |> 
   filter(month == 11 | 12)
## # A tibble: 336,776 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 336,766 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Vermeide dies, da jede Zeile dann ausgewählt wird.

flights |> 
  mutate(
    nov = month == 11,
    final = nov | 12,
    .keep = "used"
  )
## # A tibble: 336,776 × 3
##    month nov   final
##    <int> <lgl> <lgl>
##  1     1 FALSE TRUE 
##  2     1 FALSE TRUE 
##  3     1 FALSE TRUE 
##  4     1 FALSE TRUE 
##  5     1 FALSE TRUE 
##  6     1 FALSE TRUE 
##  7     1 FALSE TRUE 
##  8     1 FALSE TRUE 
##  9     1 FALSE TRUE 
## 10     1 FALSE TRUE 
## # ℹ 336,766 more rows

%in%

x %in% y gibt einen logischen Vektor der Länge von x aus, der TRUE ist, wann immer ein Wert x irgendwo in y ist.

1:12 %in% c(1, 5, 11)
##  [1]  TRUE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE
letters[1:10] %in% c("a", "e", "i", "o", "u")
##  [1]  TRUE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE  TRUE FALSE

Um alle Flüge im November und Dezember zu finden, schreibe:

flights |> 
  filter(month %in% c(11, 12))
## # A tibble: 55,403 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013    11     1        5           2359         6      352            345
##  2  2013    11     1       35           2250       105      123           2356
##  3  2013    11     1      455            500        -5      641            651
##  4  2013    11     1      539            545        -6      856            827
##  5  2013    11     1      542            545        -3      831            855
##  6  2013    11     1      549            600       -11      912            923
##  7  2013    11     1      550            600       -10      705            659
##  8  2013    11     1      554            600        -6      659            701
##  9  2013    11     1      554            600        -6      826            827
## 10  2013    11     1      554            600        -6      749            751
## # ℹ 55,393 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Hier gelten andere Regeln für NA.

c(1, 2, NA) == NA
#> [1] NA NA NA
c(1, 2, NA) %in% NA
#> [1] FALSE FALSE  TRUE

So erhalten wir:

flights |> 
  filter(dep_time %in% c(NA, 0800))
## # A tibble: 8,803 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      800            800         0     1022           1014
##  2  2013     1     1      800            810       -10      949            955
##  3  2013     1     1       NA           1630        NA       NA           1815
##  4  2013     1     1       NA           1935        NA       NA           2240
##  5  2013     1     1       NA           1500        NA       NA           1825
##  6  2013     1     1       NA            600        NA       NA            901
##  7  2013     1     2      800            810       -10     1102           1116
##  8  2013     1     2       NA           1540        NA       NA           1747
##  9  2013     1     2       NA           1620        NA       NA           1746
## 10  2013     1     2       NA           1355        NA       NA           1459
## # ℹ 8,793 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Summaries

Nützliche Techniken für das Zusammenfassen von logischen Vektoren erscheinen jetzt.

Logical Summaries

Es gibt zwei logical summaries: any() und all(). any(x) ist das Äquivalent zu |; es gibt TRUE aus, wenn es irgendein TRUE in x gibt. all(x) ist äquivalent zu &. Es gibt TRUE zurück, wenn alle Werte von x TRUE’s sind. NA wird ausgegeben, wenn es Missing Values gibt, und zu vermeiden ist dies mit na.rm = TRUE.
Gibt es Tage, an denen alle Flüge Verspätung hatten?

flights |> 
  group_by(year, month, day) |> 
  summarize(
    all_delayed = all(arr_delay >= 0, na.rm = TRUE),
    any_delayed = any(arr_delay >= 0, na.rm = TRUE),
    .groups = "drop"
  )
## # A tibble: 365 × 5
##     year month   day all_delayed any_delayed
##    <int> <int> <int> <lgl>       <lgl>      
##  1  2013     1     1 FALSE       TRUE       
##  2  2013     1     2 FALSE       TRUE       
##  3  2013     1     3 FALSE       TRUE       
##  4  2013     1     4 FALSE       TRUE       
##  5  2013     1     5 FALSE       TRUE       
##  6  2013     1     6 FALSE       TRUE       
##  7  2013     1     7 FALSE       TRUE       
##  8  2013     1     8 FALSE       TRUE       
##  9  2013     1     9 FALSE       TRUE       
## 10  2013     1    10 FALSE       TRUE       
## # ℹ 355 more rows

Numeric Summaries Of Logical Vectors

Wenn du einen logischen Vektor in einem numerischen Kontext benutzt, so wird TRUE zu 1 und FALSE zu 0. So lassen sich sum() und mean() sehr nützlich mit logischen Vektoren vereinen, da die Anzahl an TRUE’s und der Anteil ausgegeben wird.

flights |> 
  group_by(year, month, day) |> 
  summarize(
    prop_delayed = mean(arr_delay > 0, na.rm = TRUE),
    .groups = "drop"
  ) |> 
  ggplot(aes(x = prop_delayed)) + 
  geom_histogram(binwidth = 0.05)

Wieviele Flüge sind vor 5 Uhr abgeflogen?

flights |> 
  group_by(year, month, day) |> 
  summarize(
    n_early = sum(dep_time < 500, na.rm = TRUE),
    .groups = "drop"
  ) |> 
  arrange(desc(n_early))
## # A tibble: 365 × 4
##     year month   day n_early
##    <int> <int> <int>   <int>
##  1  2013     6    28      32
##  2  2013     4    10      30
##  3  2013     7    28      30
##  4  2013     3    18      29
##  5  2013     7     7      29
##  6  2013     7    10      29
##  7  2013     6    27      25
##  8  2013     6    13      24
##  9  2013     3     8      22
## 10  2013     7    22      22
## # ℹ 355 more rows

Logical Subsetting

Wir können logical vector auch in summarize benutzen, um hier eine einzelne Variable zu filtern. Die Nutzung der eckigen Klammern hilft.
Wir wollen den durchschnittlichen Delay, also Verspätung, berechnen. Also nur für Flüge mit Verspätung (>0). Eine Möglichkeit:

flights |> 
  filter(arr_delay > 0) |> 
  group_by(year, month, day) |> 
  summarize(
    behind = mean(arr_delay),
    n = n(),
    .groups = "drop"
  )
## # A tibble: 365 × 5
##     year month   day behind     n
##    <int> <int> <int>  <dbl> <int>
##  1  2013     1     1   32.5   461
##  2  2013     1     2   32.0   535
##  3  2013     1     3   27.7   460
##  4  2013     1     4   28.3   297
##  5  2013     1     5   22.6   238
##  6  2013     1     6   24.4   381
##  7  2013     1     7   27.8   243
##  8  2013     1     8   20.8   275
##  9  2013     1     9   25.6   287
## 10  2013     1    10   27.3   220
## # ℹ 355 more rows

Aber was, wenn wir auch die durchschnittliche Verspätung für Flüge berechnen wollen, die zu früh angekommen sind?

flights |> 
  group_by(year, month, day) |> 
  summarize(
    behind = mean(arr_delay[arr_delay > 0], na.rm = TRUE),
    ahead = mean(arr_delay[arr_delay < 0], na.rm = TRUE),
    n = n(),
    .groups = "drop"
  )
## # A tibble: 365 × 6
##     year month   day behind ahead     n
##    <int> <int> <int>  <dbl> <dbl> <int>
##  1  2013     1     1   32.5 -12.5   842
##  2  2013     1     2   32.0 -14.3   943
##  3  2013     1     3   27.7 -18.2   914
##  4  2013     1     4   28.3 -17.0   915
##  5  2013     1     5   22.6 -14.0   720
##  6  2013     1     6   24.4 -13.6   832
##  7  2013     1     7   27.8 -17.0   933
##  8  2013     1     8   20.8 -14.3   899
##  9  2013     1     9   25.6 -13.0   902
## 10  2013     1    10   27.3 -16.4   932
## # ℹ 355 more rows

Bedenke auch den Unterschied der Gruppengröße: delayed vs. total flights.

Konditionale Transformationen

Hier gibt es zwei mächtige Werkzeuge: if_else() und case_when().

if_else()

Mit dplyr::if_else() kannst du einen Wert benutzen, wenn die Bedingung TRUE ist und einen anderen, wenn sie FALSE ist. Das erste der drei Argumente ist die condition, ein logical vector. Dann true, wenn die Bedingung erfüllt ist, und false, der Output, falls die condition false ist.

x <- c(-3:3, NA)
if_else(x > 0, "+ve", "-ve")
## [1] "-ve" "-ve" "-ve" "-ve" "+ve" "+ve" "+ve" NA

Es gibt tatsächlich ein viertes optionales Argument, falls der Input NA ist.

if_else(x > 0, "+ve", "-ve", "???")
## [1] "-ve" "-ve" "-ve" "-ve" "+ve" "+ve" "+ve" "???"

Du kannst anstelle der true, false Argumente auch Vektoren benutzen.

if_else(x < 0, -x, x)
## [1]  3  2  1  0  1  2  3 NA
x1 <- c(NA, 1, 2, NA)
y1 <- c(3, NA, 4, 6)
if_else(is.na(x1), y1, x1)
## [1] 3 1 2 6
if_else(x == 0, "0", if_else(x < 0, "-ve", "+ve"), "???")
## [1] "-ve" "-ve" "-ve" "0"   "+ve" "+ve" "+ve" "???"

Die letzte Zeile ist hart zu lesen, deshalb wechseln wir lieber zu dplyr::case_when().

case_when()

Eine spezielle Syntax haben wir hier. Sie nimmt Paare vor: condition ~ output. condition muss ein logical vector sein. Bei Erfüllung wird output genutzt.

case_when(
  x == 0   ~ "0",
  x < 0    ~ "-ve", 
  x > 0    ~ "+ve",
  is.na(x) ~ "???"
)
## [1] "-ve" "-ve" "-ve" "0"   "+ve" "+ve" "+ve" "???"

Wenn kein Fall passt, erhalten wir ein NA.

case_when(
  x < 0 ~ "-ve",
  x > 0 ~ "+ve"
)
## [1] "-ve" "-ve" "-ve" NA    "+ve" "+ve" "+ve" NA

Für einen default, nutze TRUE auf der linken Seite.

case_when(
  x < 0 ~ "-ve",
  x > 0 ~ "+ve",
  TRUE ~ "???"
)
## [1] "-ve" "-ve" "-ve" "???" "+ve" "+ve" "+ve" "???"

Sind mehrere Bedingungen erfüllt, so wird nur der erste genutzt.

case_when(
  x > 0 ~ "+ve",
  x > 3 ~ "big"
)
## [1] NA    NA    NA    NA    "+ve" "+ve" "+ve" NA

Etwas so schönes können wir bauen:

flights |> 
  mutate(
    status = case_when(
      is.na(arr_delay)      ~ "cancelled",
      arr_delay < -30       ~ "very early",
      arr_delay < -15       ~ "early",
      abs(arr_delay) <= 15  ~ "on time",
      arr_delay > 15        ~ "late",
      arr_delay > 60        ~ "very late",
    ),
    .keep = "used"
  )
## # A tibble: 336,776 × 2
##    arr_delay status 
##        <dbl> <chr>  
##  1        11 on time
##  2        20 late   
##  3        33 late   
##  4       -18 early  
##  5       -25 early  
##  6        12 on time
##  7        19 late   
##  8       -14 on time
##  9        -8 on time
## 10         8 on time
## # ℹ 336,766 more rows

Kompatible Typen

ifelse() und case_when() erfordern passende Typen. Passen sie nicht, gibt es Fehlermeldungen.

if_else(c(TRUE,FALSE), "a", 2)

Kompatibel sind:

  • numerische und logische Vektoren.
  • Strings und Faktoren. Denke bei einem Faktor an einen String mit begrenzten Werten.
  • Dates und date-times.
  • NA-ä- ist kompatibel mit allem.

Transformieren: Zahlen

Einleitung

Das Rückgrat der Data Science sind natürlich Zahlen. Was kannst du in R alles mit ihnen machen?

Voraussetzungen

library(tidyverse)
library(nycflights13)

Zahlen machen

readr bietet zwei wichtige Funktionen für die Transformation von Strings in Zahlen: parse_double() und parse_number(). Benutze parse_double(), wenn du Zahlen als Strings geschrieben hast.

x <- c("1.2", "5.6", "1e3")
parse_double(x)
## [1]    1.2    5.6 1000.0

Benutze parse_number(), wenn dein String nicht-numerischen Text enthält, den du ignorieren willst.

x <- c("$1,234", "USD 3,513", "59%")
parse_number(x)
## [1] 1234 3513   59

Counts

Für schnelle Entdeckungen und Checken ist diese Funktion count() genial. Auch sortieren geht rasch.

flights |>
  print(n = 10)
## # A tibble: 336,776 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      533            529         4      850            830
##  3  2013     1     1      542            540         2      923            850
##  4  2013     1     1      544            545        -1     1004           1022
##  5  2013     1     1      554            600        -6      812            837
##  6  2013     1     1      554            558        -4      740            728
##  7  2013     1     1      555            600        -5      913            854
##  8  2013     1     1      557            600        -3      709            723
##  9  2013     1     1      557            600        -3      838            846
## 10  2013     1     1      558            600        -2      753            745
## # ℹ 336,766 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>
flights |> count(dest)
## # A tibble: 105 × 2
##    dest      n
##    <chr> <int>
##  1 ABQ     254
##  2 ACK     265
##  3 ALB     439
##  4 ANC       8
##  5 ATL   17215
##  6 AUS    2439
##  7 AVL     275
##  8 BDL     443
##  9 BGR     375
## 10 BHM     297
## # ℹ 95 more rows
flights |> count(dest, sort = TRUE)
## # A tibble: 105 × 2
##    dest      n
##    <chr> <int>
##  1 ORD   17283
##  2 ATL   17215
##  3 LAX   16174
##  4 BOS   15508
##  5 MCO   14082
##  6 CLT   14064
##  7 SFO   13331
##  8 FLL   12055
##  9 MIA   11728
## 10 DCA    9705
## # ℹ 95 more rows

Dieselbe Berechnung kannst du per Hand mit group_by() und summarize() und n() vornehmen. Jetzt kannst du auch andere Summaries vornehmen.

flights |> 
  group_by(dest) |> 
  summarize(
    n = n(),
    delay = mean(arr_delay, na.rm = TRUE)
  )
## # A tibble: 105 × 3
##    dest      n delay
##    <chr> <int> <dbl>
##  1 ABQ     254  4.38
##  2 ACK     265  4.85
##  3 ALB     439 14.4 
##  4 ANC       8 -2.5 
##  5 ATL   17215 11.3 
##  6 AUS    2439  6.02
##  7 AVL     275  8.00
##  8 BDL     443  7.05
##  9 BGR     375  8.03
## 10 BHM     297 16.9 
## # ℹ 95 more rows

n() funktioniert natürlich nur in der dplyr Umgebung und brauch keine Argumente. Es gibt jedoch ein paar Varianten von n():

  • n_distinct(x) zählt die Anzahl einzigartiger Werte einer oder mehrerer Variablen. Welche Ziele werden von den meisten Fluglinien angesteuert?
flights |> 
  group_by(dest) |> 
  summarize(
    carriers = n_distinct(carrier)
  ) |> 
  arrange(desc(carriers))
## # A tibble: 105 × 2
##    dest  carriers
##    <chr>    <int>
##  1 ATL          7
##  2 BOS          7
##  3 CLT          7
##  4 ORD          7
##  5 TPA          7
##  6 AUS          6
##  7 DCA          6
##  8 DTW          6
##  9 IAD          6
## 10 MSP          6
## # ℹ 95 more rows
  • Ein gewichteter Count ist eine Summe. Wieviel Meilen ist jedes Flugzeug geflogen?
flights |> 
  group_by(tailnum) |> 
  summarize(miles = sum(distance))
## # A tibble: 4,044 × 2
##    tailnum  miles
##    <chr>    <dbl>
##  1 D942DN    3418
##  2 N0EGMQ  250866
##  3 N10156  115966
##  4 N102UW   25722
##  5 N103US   24619
##  6 N104UW   25157
##  7 N10575  150194
##  8 N105UW   23618
##  9 N107US   21677
## 10 N108UW   32070
## # ℹ 4,034 more rows

count() mit dem Argument wt macht dasselbe.

flights |> count(tailnum, wt = distance)
## # A tibble: 4,044 × 2
##    tailnum      n
##    <chr>    <dbl>
##  1 D942DN    3418
##  2 N0EGMQ  250866
##  3 N10156  115966
##  4 N102UW   25722
##  5 N103US   24619
##  6 N104UW   25157
##  7 N10575  150194
##  8 N105UW   23618
##  9 N107US   21677
## 10 N108UW   32070
## # ℹ 4,034 more rows

Missing Values kannst du zählen durch Kombinieren von sum() und is.na(). Im flights Datensatz sind es die gecancellten Flüge.

flights |> 
  group_by(dest) |> 
  summarize(n_cancelled = sum(is.na(dep_time))) 
## # A tibble: 105 × 2
##    dest  n_cancelled
##    <chr>       <int>
##  1 ABQ             0
##  2 ACK             0
##  3 ALB            20
##  4 ANC             0
##  5 ATL           317
##  6 AUS            21
##  7 AVL            12
##  8 BDL            31
##  9 BGR            15
## 10 BHM            25
## # ℹ 95 more rows

Numerische Transformationen

Transformationsfunktionen funktionieren sehr gut mit mutate(), weil ihr Output dieselbe Länge wie ihr Input hat.

Arithmetische und recycling Regeln

Die Basics wie Addition usw. sind bekannt. Was aber passiert, wenn linke und rechte Seite verschiedene Längen haben? flights |> mutate(air_time = air_time / 60). 336 766 Zahlen links und eine rechts. Der kürzere Vektor wird wiederholt.

x <- c(1, 2, 10, 20)
x / 5
## [1] 0.2 0.4 2.0 4.0
x / c(5, 5, 5, 5)
## [1] 0.2 0.4 2.0 4.0

Eine Warnung wird ausgegeben, wenn der längere kein Vielfaches des kürzeren Vektors ist.

x * c(1, 2, 3)
#> Warning in x * c(1, 2, 3): longer object length is not a multiple of shorter

Diese recycling rules werden auch auf logische Vergleiche angewendet (==, <, <=, >, >=, !=) und führen zu überraschenden Ergebnissen, wenn man fälschlicherweise == statt %in% anwendet.

flights |> 
  filter(month == c(1, 2))
## # A tibble: 25,977 × 19
##     year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
##    <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
##  1  2013     1     1      517            515         2      830            819
##  2  2013     1     1      542            540         2      923            850
##  3  2013     1     1      554            600        -6      812            837
##  4  2013     1     1      555            600        -5      913            854
##  5  2013     1     1      557            600        -3      838            846
##  6  2013     1     1      558            600        -2      849            851
##  7  2013     1     1      558            600        -2      924            917
##  8  2013     1     1      559            600        -1      941            910
##  9  2013     1     1      559            600        -1      854            902
## 10  2013     1     1      600            600         0      837            825
## # ℹ 25,967 more rows
## # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
## #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
## #   hour <dbl>, minute <dbl>, time_hour <dttm>

Es findet natürlich nur Flüge in ungeraden Zeilen, die im Januar, und Flüge in geraden Zeilen, die im Februar geflogen sind. Um uns vor solchen Fehlern zu schützen: die meisten tidyverse Funktionen benutzen eine striktere Form von Recycling, das nur einzelne Werte recyclt.

Minimum und Maximum

Arithmetische Funktionen funktionieren gut mit Paaren von Variablen. pmin() und pmax() geben den kleinsten oder größten Wert in jeder Zeile zurück.

df <- tribble(
  ~x, ~y,
  1,  3,
  5,  2,
  7, NA,
)

df |> 
  mutate(
    min = pmin(x, y, na.rm = TRUE),
    max = pmax(x, y, na.rm = TRUE)
  )
## # A tibble: 3 × 4
##       x     y   min   max
##   <dbl> <dbl> <dbl> <dbl>
## 1     1     3     1     3
## 2     5     2     2     5
## 3     7    NA     7     7

Sie sind ungleich min() und max().

df |> 
  mutate(
    min = min(x, y, na.rm = TRUE),
    max = max(x, y, na.rm = TRUE)
  )
## # A tibble: 3 × 4
##       x     y   min   max
##   <dbl> <dbl> <dbl> <dbl>
## 1     1     3     1     7
## 2     5     2     1     7
## 3     7    NA     1     7

Modulare Arithmetic

1:10 %/% 3 # Ganze Zahl bei Division vor dem Komma
##  [1] 0 0 1 1 1 2 2 2 3 3
1:10 %% 3 # Nachkommazahl bzw. Rest
##  [1] 1 2 0 1 2 0 1 2 0 1

Moduare Arithmetic ist für die Flüge nützlich, da die Zeitvarable sched_dep_time in hour und minute umgerechnet werden kann:

flights |> 
  mutate(
    hour = sched_dep_time %/% 100,
    minute = sched_dep_time %% 100,
    .keep = "used"
  )
## # A tibble: 336,776 × 3
##    sched_dep_time  hour minute
##             <int> <dbl>  <dbl>
##  1            515     5     15
##  2            529     5     29
##  3            540     5     40
##  4            545     5     45
##  5            600     6      0
##  6            558     5     58
##  7            600     6      0
##  8            600     6      0
##  9            600     6      0
## 10            600     6      0
## # ℹ 336,766 more rows

So können wir den Anteil der gestrichenen Flüge über den Tag berechnen.

flights |> 
  group_by(hour = sched_dep_time %/% 100) |> 
  summarize(prop_cancelled = mean(is.na(dep_time)), n = n()) |> 
  filter(hour > 1) |> 
  ggplot(aes(x = hour, y = prop_cancelled)) +
  geom_line(color = "grey50") + 
  geom_point(aes(size = n))

Logarithmus

starting <- 100
interest <- 1.05

money <- tibble(
  year = 1:50,
  money = starting * interest ^ year
)

Eine Exponentialkurve zeigt dein Geldwachstum an.

ggplot(money, aes(x = year, y = money)) +
  geom_line()

Logarithmische Transformation der y-Achse zeigt eine gerade Linie.

ggplot(money, aes(x = year, y = money)) +
  geom_line() + 
  scale_y_log10()

Runden

Benutze round(x) zum Runden zum nächsten Nachbarn.

round(123.456)
## [1] 123

Präzisiere das Ganze über ein zweites Argument.

round(123.456, 2)  # two digits
## [1] 123.46
round(123.456, 1)  # one digit
## [1] 123.5
round(123.456, -1) # round to nearest ten
## [1] 120
round(123.456, -2) # round to nearest hundred
## [1] 100

Es wird bei .5 immer zur geraden Zahl gerundet. Abrunden und Aufrunden durch floor() und ceiling().

x <- 123.456
floor(x)
## [1] 123
ceiling(x)
## [1] 124

Zahlen in Intervalle einteilen

x <- c(1, 2, 5, 10, 15, 20)
cut(x, breaks = c(0, 5, 10, 15, 20))
## [1] (0,5]   (0,5]   (0,5]   (5,10]  (10,15] (15,20]
## Levels: (0,5] (5,10] (10,15] (15,20]
cut(x, breaks = c(0, 5, 10, 100))
## [1] (0,5]    (0,5]    (0,5]    (5,10]   (10,100] (10,100]
## Levels: (0,5] (5,10] (10,100]

Benutze deine eigenen Labels, aber immer eins weniger als breaks.

cut(x, 
  breaks = c(0, 5, 10, 15, 20), 
  labels = c("sm", "md", "lg", "xl")
)
## [1] sm sm sm md lg xl
## Levels: sm md lg xl

Jeder Wert, der nicht im Intervall ist, bekommt ein NA.

y <- c(NA, -10, 5, 10, 30)
cut(y, breaks = c(0, 5, 10, 15, 20))
## [1] <NA>   <NA>   (0,5]  (5,10] <NA>  
## Levels: (0,5] (5,10] (10,15] (15,20]

Kummulieren und Aggregieren

x <- 1:10
cumsum(x)
##  [1]  1  3  6 10 15 21 28 36 45 55

Im slider Paket gibt es komplexere Aggregate.

Generelle Transformationen

Ränge

dplyr::min_rank() behandelt Bindungen als 1. 2. 2. 4.

x <- c(1, 4, 2, 3, 2, NA)
min_rank(x)
## [1]  1  5  2  4  2 NA
min_rank(desc(x))
## [1]  5  1  3  2  3 NA

Ähnliche Varianten findest du in der Dokumentation.

df <- tibble(x = x)
df |> 
  mutate(
    row_number = row_number(x),
    dense_rank = dense_rank(x),
    percent_rank = percent_rank(x),
    cume_dist = cume_dist(x)
  )
## # A tibble: 6 × 5
##       x row_number dense_rank percent_rank cume_dist
##   <dbl>      <int>      <int>        <dbl>     <dbl>
## 1     1          1          1         0          0.2
## 2     4          5          4         1          1  
## 3     2          2          2         0.25       0.6
## 4     3          4          3         0.75       0.8
## 5     2          3          2         0.25       0.6
## 6    NA         NA         NA        NA         NA

Verschiebungen

x <- c(2, 5, 11, 11, 19, 35)
lag(x) # Verschebung eins nach rechts
## [1] NA  2  5 11 11 19
lead(x) # Verschiebung eins nach links
## [1]  5 11 11 19 35 NA
x - lag(x) # Differenz zwischen aktuellem Wert und Vorgänger
## [1] NA  3  6  0  8 16
x == lag(x) # wechselt der aktuelle Wert?
## [1]    NA FALSE FALSE  TRUE FALSE FALSE

Über ein zweites Agument kannst du den “Lag” per Hand bestimmen.

consecutive_id()

Zeiten, wann eine Website besucht wird:

events <- tibble(
  time = c(0, 1, 2, 3, 5, 10, 12, 15, 17, 19, 20, 27, 28, 30)
)

Zeit zwischen zwei Besuchen, “gap” größer gleich 5 soll identifiziert werden.

events <- events |> 
  mutate(
    diff = time - lag(time, default = first(time)),
    gap = diff >= 5
  )
events
## # A tibble: 14 × 3
##     time  diff gap  
##    <dbl> <dbl> <lgl>
##  1     0     0 FALSE
##  2     1     1 FALSE
##  3     2     1 FALSE
##  4     3     1 FALSE
##  5     5     2 FALSE
##  6    10     5 TRUE 
##  7    12     2 FALSE
##  8    15     3 FALSE
##  9    17     2 FALSE
## 10    19     2 FALSE
## 11    20     1 FALSE
## 12    27     7 TRUE 
## 13    28     1 FALSE
## 14    30     2 FALSE

Vom logischen Vektor wollen wir zu einer Gruppierungsvariable: mit cur_group_id():

events <- events |>
group_by(gap) |>
mutate(group = cur_group_id())
events
## # A tibble: 14 × 4
## # Groups:   gap [2]
##     time  diff gap   group
##    <dbl> <dbl> <lgl> <int>
##  1     0     0 FALSE     1
##  2     1     1 FALSE     1
##  3     2     1 FALSE     1
##  4     3     1 FALSE     1
##  5     5     2 FALSE     1
##  6    10     5 TRUE      2
##  7    12     2 FALSE     1
##  8    15     3 FALSE     1
##  9    17     2 FALSE     1
## 10    19     2 FALSE     1
## 11    20     1 FALSE     1
## 12    27     7 TRUE      2
## 13    28     1 FALSE     1
## 14    30     2 FALSE     1

Numerische Zusammenfassungen

Zentrieren

mean() vs. median(). Je nach Ausreißer und Form erhalten wir verschiedenen Ergebnisse. Bedenke die Einkommensverteilung. Hier ist der mean mit Sicherheit größer.
Bei unseren Flugverspätungen eben.

flights |>
  group_by(year, month, day) |>
  summarize(
    mean = mean(dep_delay, na.rm = TRUE),
    median = median(dep_delay, na.rm = TRUE),
    n = n(),
    .groups = "drop"
  ) |> 
  ggplot(aes(x = mean, y = median)) + 
  geom_abline(slope = 1, intercept = 0, color = "white", size = 2) +
  geom_point()

flights |>
  group_by(year, month, day) |>
  summarize(
    mean = mean(dep_delay, na.rm = TRUE),
    median = median(dep_delay, na.rm = TRUE),
    n = n(),
    .groups = "drop"
  )
## # A tibble: 365 × 6
##     year month   day  mean median     n
##    <int> <int> <int> <dbl>  <dbl> <int>
##  1  2013     1     1 11.5      -1   842
##  2  2013     1     2 13.9       0   943
##  3  2013     1     3 11.0       0   914
##  4  2013     1     4  8.95     -1   915
##  5  2013     1     5  5.73     -1   720
##  6  2013     1     6  7.15     -1   832
##  7  2013     1     7  5.42     -2   933
##  8  2013     1     8  2.55     -2   899
##  9  2013     1     9  2.28     -4   902
## 10  2013     1    10  2.84     -4   932
## # ℹ 355 more rows

Minimum, Maximum und Quantile

quantile(x, 0.25), quantile(x, 0.5), quantile(x, 0.95) sind selbsterklärend.

flights |>
  group_by(year, month, day) |>
  summarize(
    max = max(dep_delay, na.rm = TRUE),
    q95 = quantile(dep_delay, 0.95, na.rm = TRUE),
    .groups = "drop"
  )
## # A tibble: 365 × 5
##     year month   day   max   q95
##    <int> <int> <int> <dbl> <dbl>
##  1  2013     1     1   853  70.1
##  2  2013     1     2   379  85  
##  3  2013     1     3   291  68  
##  4  2013     1     4   288  60  
##  5  2013     1     5   327  41  
##  6  2013     1     6   202  51  
##  7  2013     1     7   366  51.6
##  8  2013     1     8   188  35.3
##  9  2013     1     9  1301  27.2
## 10  2013     1    10  1126  31  
## # ℹ 355 more rows

Streuung

Standardabweichung sd(x), Interquartilsabstand IQR(). IGQ ist das 75% - 25% Quantil.

flights |> 
  group_by(origin, dest) |> 
  summarize(
    distance_sd = IQR(distance), 
    n = n(),
    .groups = "drop"
  ) |> 
  filter(distance_sd > 0)
## # A tibble: 2 × 4
##   origin dest  distance_sd     n
##   <chr>  <chr>       <dbl> <int>
## 1 EWR    EGE             1   110
## 2 JFK    EGE             1   103

Verteilungen

Visualisiere die Verteilung bevor du zusammenfassende Statistiken berechnest.

flights |>
  ggplot(aes(x = dep_delay)) + 
  geom_histogram(binwidth = 15)
## Warning: Removed 8255 rows containing non-finite values (`stat_bin()`).

flights |>
  filter(dep_delay < 120) |> 
  ggplot(aes(x = dep_delay)) + 
  geom_histogram(binwidth = 5)

Checke, ob die Untergruppen die ganze Verteilung für alle wiederspiegeln.

flights |>
  filter(dep_delay < 120) |> 
  ggplot(aes(x = dep_delay, group = interaction(day, month))) + 
  geom_freqpoly(binwidth = 5, alpha = 1/5)

Positionen

Es gibt drei Funktionen, die man benutzen kann, um Werte zu extrahieren, die an einer speziellen Position stehen: first(x), last(x), nth(x).

flights |> 
  group_by(year, month, day) |> 
  summarize(
    first_dep = first(dep_time), 
    fifth_dep = nth(dep_time, 5),
    last_dep = last(dep_time)
  )

Hier fehlt ein na.rm = T, daher benutze ich auf den ganzen Datensatz ein na.omit().

flights |> 
  na.omit() |>
  group_by(year, month, day) |> 
  summarize(
    last_dep = last(dep_time)
  ) 
## `summarise()` has grouped output by 'year', 'month'. You can override using the
## `.groups` argument.
## # A tibble: 365 × 4
## # Groups:   year, month [12]
##     year month   day last_dep
##    <int> <int> <int>    <int>
##  1  2013     1     1     2356
##  2  2013     1     2     2354
##  3  2013     1     3     2349
##  4  2013     1     4     2358
##  5  2013     1     5     2357
##  6  2013     1     6     2355
##  7  2013     1     7     2359
##  8  2013     1     8     2351
##  9  2013     1     9     2252
## 10  2013     1    10     2320
## # ℹ 355 more rows

Werte aus Positionen ziehen, ist komplementär zu Filtern auf Rängen. Filtern gibt uns alle Variablen, mit jeder Beobachtung in einer eigenen Reihe.

flights |> 
  group_by(year, month, day) |> 
  mutate(r = min_rank(desc(sched_dep_time)), .keep = "used") |> 
  filter(r %in% c(1, max(r)))
## # A tibble: 1,195 × 5
## # Groups:   year, month, day [365]
##     year month   day sched_dep_time     r
##    <int> <int> <int>          <int> <int>
##  1  2013     1     1            515   842
##  2  2013     1     1           2359     1
##  3  2013     1     1           2359     1
##  4  2013     1     1           2359     1
##  5  2013     1     2           2359     1
##  6  2013     1     2            500   943
##  7  2013     1     2           2359     1
##  8  2013     1     2           2359     1
##  9  2013     1     3           2359     1
## 10  2013     1     3           2359     1
## # ℹ 1,185 more rows

Mit mutate()

Summary Functions werden normalerweise mit summarize() gepaart. Aufgrund der* recycling rules aber auch mit mutate(), besonders bei Standardisierung:

  • x / sum(x) - Anteil.
  • (x - mean(x)) / sd(x) - Z-Score.
  • x / first(x) - Index basierend auf erster Beobachtung.

Strings

Einleitung

Hier lernen wir etwas über String-Manipulationswerkzeuge.

Voraussetzungen

library(babynames)
## Warning: Paket 'babynames' wurde unter R Version 4.2.3 erstellt

Das stringr Paket ist Teil des tidyverse. Alle stringr Funktionen starten mit str_.

Einen String kreieren

string1 <- "This is a string"
string2 <- 'If I want to include a "quote" inside a string, I use single quotes'

Vergisst du einen Ausdruck zu schließen, so erscheint ein +.

Escapes

double_quote <- "\"" # or '"'
single_quote <- '\'' # or "'"
backslash <- "\\"
x <- c(single_quote, double_quote, backslash)
x
## [1] "'"  "\"" "\\"
str_view(x)
## [1] │ '
## [2] │ "
## [3] │ \

Rohe Strings

tricky <- "double_quote <- \"\\\"\" # or '\"'
single_quote <- '\\'' # or \"'\""
str_view(tricky)
## [1] │ double_quote <- "\"" # or '"'
##     │ single_quote <- '\'' # or "'"
#> [1] │ double_quote <- "\"" # or '"'
#>     │ single_quote <- '\'' # or "'"

Zu viele Backslashs. Statt dessen, nutze einen raw string durch r"( und beende mit )".

tricky <- r"(double_quote <- "\"" # or '"'
single_quote <- '\'' # or "'")"
str_view(tricky)
## [1] │ double_quote <- "\"" # or '"'
##     │ single_quote <- '\'' # or "'"
#> [1] │ double_quote <- "\"" # or '"'
#>     │ single_quote <- '\'' # or "'"

Weitere spezielle Characters

\n - neue Zeile.
\t - Tab. \u \U - nicht-englische Characters.

x <- c("one\ntwo", "one\ttwo", "\u00b5", "\U0001f604")
x
## [1] "one\ntwo" "one\ttwo" "µ"        "😄"
#> [1] "one\ntwo" "one\ttwo" "µ"        "😄"
str_view(x)
## [1] │ one
##     │ two
## [2] │ one{\t}two
## [3] │ µ
## [4] │ 😄
#> [1] │ one
#>     │ two
#> [2] │ one{\t}two
#> [3] │ µ
#> [4] │ 😄

Viele Strings kreieren

str_c()

Nimmt Vektoren an und gibt einen Character Vector aus.

str_c("x", "y")
## [1] "xy"
str_c("x", "y", "z")
## [1] "xyz"
str_c("Hello ", c("John", "Susan"))
## [1] "Hello John"  "Hello Susan"
str_c(c("Hallo ", "Servus "), c("Andi ", "Andrea "), c("Ciao", "Servus"), c(" Adios", " Cheers"))
## [1] "Hallo Andi Ciao Adios"       "Servus Andrea Servus Cheers"

str_c() ist designt, um mit mutate() genutzt werden und gehorcht so den gewöhnlichen Regeln für recycling und Missing Values.

set.seed(1410)
df <- tibble(name = c(wakefield::name(3), NA))
df |> mutate(greeting = str_c("Hi ", name, "!"))
## # A tibble: 4 × 2
##   name       greeting      
##   <chr>      <chr>         
## 1 Ilena      Hi Ilena!     
## 2 Sacramento Hi Sacramento!
## 3 Graylon    Hi Graylon!   
## 4 <NA>       <NA>

Benutze coalesce(), um statt Missing Values Alternativen ausgeben zu lassen.

df |> 
  mutate(
    greeting1 = str_c("Hi ", coalesce(name, "you"), "!"),
    greeting2 = coalesce(str_c("Hi ", name, "!"), "Hi!")
  )
## # A tibble: 4 × 3
##   name       greeting1      greeting2     
##   <chr>      <chr>          <chr>         
## 1 Ilena      Hi Ilena!      Hi Ilena!     
## 2 Sacramento Hi Sacramento! Hi Sacramento!
## 3 Graylon    Hi Graylon!    Hi Graylon!   
## 4 <NA>       Hi you!        Hi!

str_glue()

Viele Gänsefüßchen mussten wir verwenden. Dies ist vermeidbar, wenn wir str_glue() aus dem glue Paket benutzen. Alles innerhalb {} wird bewertet, als wäre es außerhalb der Anführungszeichen.

df |> mutate(greeting = str_glue("Hi {name}!"))
## # A tibble: 4 × 2
##   name       greeting      
##   <chr>      <glue>        
## 1 Ilena      Hi Ilena!     
## 2 Sacramento Hi Sacramento!
## 3 Graylon    Hi Graylon!   
## 4 <NA>       Hi NA!

str_glue() konvertiert Missing Values zu "NA". Jetzt ist es inkonsistent zu str_c(). Wenn du { oder } einfühgen willst, nutze {{ bzw. }}.

df |> mutate(greeting = str_glue("{{Hi {name}!}}"))
## # A tibble: 4 × 2
##   name       greeting        
##   <chr>      <glue>          
## 1 Ilena      {Hi Ilena!}     
## 2 Sacramento {Hi Sacramento!}
## 3 Graylon    {Hi Graylon!}   
## 4 <NA>       {Hi NA!}

str_flatten()

str_c() und glue() funktionieren gut mit mutate(), weil ihr Output dieselbe Länge wie Input hat. Was aber, wenn du nur einen einzelnen String ausgegeben haben willst. str_flatten() nimmt einen Character Vektor und kombiniert jedes Element des Vektors in einen single String:

detach("package:tidyverse", unload = TRUE)
library(tidytable)
## Warning: Paket 'tidytable' wurde unter R Version 4.2.3 erstellt
## Warning: tidytable was loaded after dplyr.
## This can lead to most dplyr functions being overwritten by tidytable functions.
## Warning: tidytable was loaded after tidyr.
## This can lead to most tidyr functions being overwritten by tidytable functions.
## 
## Attache Paket: 'tidytable'
## Die folgenden Objekte sind maskiert von 'package:dplyr':
## 
##     across, add_count, add_tally, anti_join, arrange, between,
##     bind_cols, bind_rows, c_across, case_match, case_when, coalesce,
##     consecutive_id, count, cross_join, cume_dist, cur_column, cur_data,
##     cur_group_id, cur_group_rows, dense_rank, desc, distinct, filter,
##     first, full_join, group_by, group_cols, group_split, group_vars,
##     if_all, if_any, if_else, inner_join, is_grouped_df, lag, last,
##     lead, left_join, min_rank, mutate, n, n_distinct, na_if, nest_by,
##     nest_join, nth, percent_rank, pick, pull, recode, reframe,
##     relocate, rename, rename_with, right_join, row_number, rowwise,
##     select, semi_join, slice, slice_head, slice_max, slice_min,
##     slice_sample, slice_tail, summarise, summarize, tally, top_n,
##     transmute, tribble, ungroup
## Die folgenden Objekte sind maskiert von 'package:purrr':
## 
##     map, map_chr, map_dbl, map_df, map_dfc, map_dfr, map_int, map_lgl,
##     map_vec, map2, map2_chr, map2_dbl, map2_df, map2_dfc, map2_dfr,
##     map2_int, map2_lgl, map2_vec, pmap, pmap_chr, pmap_dbl, pmap_df,
##     pmap_dfc, pmap_dfr, pmap_int, pmap_lgl, pmap_vec, walk
## Die folgenden Objekte sind maskiert von 'package:tidyr':
## 
##     complete, crossing, drop_na, expand, expand_grid, extract, fill,
##     nest, nesting, pivot_longer, pivot_wider, replace_na, separate,
##     separate_longer_delim, separate_rows, separate_wider_delim,
##     separate_wider_regex, tribble, uncount, unite, unnest,
##     unnest_longer, unnest_wider
## Die folgenden Objekte sind maskiert von 'package:tibble':
## 
##     enframe, tribble
## Die folgenden Objekte sind maskiert von 'package:stats':
## 
##     dt, filter, lag
## Das folgende Objekt ist maskiert 'package:base':
## 
##     %in%
str_flatten(c("x", "y", "z"))
## [1] "xyz"
str_flatten(c("x", "y", "z"), ", ")
## [1] "x, y, z"
# str_flatten(c("x", "y", "z"), ", ", last = ", and ")

Dadurch lässt es sich gut mit summarize() arbeiten:

df <- tribble(
  ~ name, ~ fruit,
  "Carmen", "banana",
  "Carmen", "apple",
  "Marvin", "nectarine",
  "Terence", "cantaloupe",
  "Terence", "papaya",
  "Terence", "madarine"
)
df |>
  group_by(name) |> 
  summarize(fruits = str_flatten(fruit, ", "))
## # A tidytable: 3 × 2
##   name    fruits                      
##   <chr>   <chr>                       
## 1 Carmen  banana, apple               
## 2 Marvin  nectarine                   
## 3 Terence cantaloupe, papaya, madarine

Daten aus Strings ziehen

  • df |> separate_longer_delim(col, delim)
  • df |> separate_longer_position(col, width)
  • df |> separate_wider_delim(col, delim, names)
  • df |> separate_wider_position(col, widths)

Separieren in Reihen

df1 <- tibble(x = c("a,b,c", "d,e", "f"))
df1 |> 
  separate_longer_delim(x, delim = ",")
## # A tidytable: 6 × 1
##   x    
##   <chr>
## 1 a    
## 2 b    
## 3 c    
## 4 d    
## 5 e    
## 6 f
#> # A tibble: 6 × 1
#>   x    
#>   <chr>
#> 1 a    
#> 2 b    
#> 3 c    
#> 4 d    
#> 5 e    
#> 6 f
df2 <- tibble(x = c("1211", "131", "21"))
df2 |> 
  separate_longer_position(x, width = 1)
## # A tibble: 9 × 1
##   x    
##   <chr>
## 1 1    
## 2 2    
## 3 1    
## 4 1    
## 5 1    
## 6 3    
## 7 1    
## 8 2    
## 9 1
#> # A tibble: 9 × 1
#>   x    
#>   <chr>
#> 1 1    
#> 2 2    
#> 3 1    
#> 4 1    
#> 5 1    
#> 6 3    
#> # … with 3 more rows

Separieren in Spalten

df3 <- tibble(x = c("a10.1.2022", "b10.2.2011", "e15.1.2015"))
df3 |> 
  separate_wider_delim(
    x,
    delim = ".",
    names = c("code", "edition", "year")
  )
## # A tidytable: 3 × 3
##   code  edition year 
##   <chr> <chr>   <chr>
## 1 a10   1       2022 
## 2 b10   2       2011 
## 3 e15   1       2015
#> # A tibble: 3 × 3
#>   code  edition year 
#>   <chr> <chr>   <chr>
#> 1 a10   1       2022 
#> 2 b10   2       2011 
#> 3 e15   1       2015
df3 |> 
  separate_wider_delim(
    x,
    delim = ".",
    names = c("code", NA, "year")
  )
## # A tidytable: 3 × 2
##   code  year 
##   <chr> <chr>
## 1 a10   2022 
## 2 b10   2011 
## 3 e15   2015
#> # A tibble: 3 × 2
#>   code  year 
#>   <chr> <chr>
#> 1 a10   2022 
#> 2 b10   2011 
#> 3 e15   2015
df4 <- tibble(x = c("202215TX", "202122LA", "202325CA")) 
df4 |> 
  separate_wider_position(
    x,
    widths = c(year = 4, age = 2, state = 2)
  )
## # A tibble: 3 × 3
##   year  age   state
##   <chr> <chr> <chr>
## 1 2022  15    TX   
## 2 2021  22    LA   
## 3 2023  25    CA
#> # A tibble: 3 × 3
#>   year  age   state
#>   <chr> <chr> <chr>
#> 1 2022  15    TX   
#> 2 2021  22    LA   
#> 3 2023  25    CA

separate_wider_delim()

df <- tibble(x = c("1-1-1", "1-1-2", "1-3", "1-3-2", "1"))

df |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z")
  )
## # A tidytable: 5 × 3
##   x     y     z    
##   <chr> <chr> <chr>
## 1 1     1     1    
## 2 1     1     2    
## 3 1     3     <NA> 
## 4 1     3     2    
## 5 1     <NA>  <NA>
df <- tibble(x = c("1-1-1", "1-1-2", "1-3-5-6", "1-3-2", "1-3-5-7-9"))

df |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z")
  )
## # A tidytable: 5 × 3
##   x     y     z    
##   <chr> <chr> <chr>
## 1 1     1     1    
## 2 1     1     2    
## 3 1     3     5    
## 4 1     3     2    
## 5 1     3     5
detach("package:tidytable", unload = TRUE)
library(tidyverse)
## Warning: Paket 'tidyverse' wurde unter R Version 4.2.3 erstellt
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ scales::col_factor() masks readr::col_factor()
## ✖ scales::discard()    masks purrr::discard()
## ✖ arrow::duration()    masks lubridate::duration()
## ✖ dplyr::filter()      masks stats::filter()
## ✖ recipes::fixed()     masks stringr::fixed()
## ✖ dplyr::lag()         masks stats::lag()
## ✖ yardstick::spec()    masks readr::spec()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
df
## # A tibble: 5 × 1
##   x        
##   <chr>    
## 1 1-1-1    
## 2 1-1-2    
## 3 1-3-5-6  
## 4 1-3-2    
## 5 1-3-5-7-9
df |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z"),
    too_many = "drop"
  )
## # A tibble: 5 × 3
##   x     y     z    
##   <chr> <chr> <chr>
## 1 1     1     1    
## 2 1     1     2    
## 3 1     3     5    
## 4 1     3     2    
## 5 1     3     5
df |> 
  separate_wider_delim(
    x,
    delim = "-",
    names = c("x", "y", "z"),
    too_many = "merge"
  )
## # A tibble: 5 × 3
##   x     y     z    
##   <chr> <chr> <chr>
## 1 1     1     1    
## 2 1     1     2    
## 3 1     3     5-6  
## 4 1     3     2    
## 5 1     3     5-7-9

Buchstaben

In diesem Abschnitt stellen wir Funktionen vor, die es uns erlauben die Buchstaben innerhalb eines Strings zu bearbeiten.

Länge

str_length() gibt die Anzahl der Buchstaben eines Strings aus.

str_length(c("a", "R for data science", NA))
## [1]  1 18 NA

Du kannst die count() Funktion nutzen, um die Anzahl der Buchstaben von Babynamen zu bestimmen.

library(babynames)
babynames |>
  count(length = str_length(name), wt = n)
## # A tibble: 14 × 2
##    length        n
##     <int>    <int>
##  1      2   338150
##  2      3  8589596
##  3      4 48506739
##  4      5 87011607
##  5      6 90749404
##  6      7 72120767
##  7      8 25404066
##  8      9 11926551
##  9     10  1306159
## 10     11  2135827
## 11     12    16295
## 12     13    10845
## 13     14     3681
## 14     15      830
babynames |> 
  filter(str_length(name) == 15) |> 
  count(name, wt = n, sort = TRUE)
## # A tibble: 34 × 2
##    name                n
##    <chr>           <int>
##  1 Franciscojavier   123
##  2 Christopherjohn   118
##  3 Johnchristopher   118
##  4 Christopherjame   108
##  5 Christophermich    52
##  6 Ryanchristopher    45
##  7 Mariadelosangel    28
##  8 Jonathanmichael    25
##  9 Christianjoseph    22
## 10 Christopherjose    22
## # ℹ 24 more rows

Subsetting

Sage bitte, wie die ersten drei Buchstaben eines Substrings lauten (str_sub(string, start, end)).

x <- c("Apple", "Banana", "Pear")
str_sub(x, 1, 3)
## [1] "App" "Ban" "Pea"

Negative Zahlen kannst du benutzen, um vom Ende zurück zu zählen.

str_sub(x, -3, -1)
## [1] "ple" "ana" "ear"

Wir können str_sub() mit mutate() benutzen, um den ersten und letzten Buchstaben eines jeden Namens zu finden.

babynames |> 
  mutate(
    first = str_sub(name, 1, 1),
    last = str_sub(name, -1, -1)
  )
## # A tibble: 1,924,665 × 7
##     year sex   name          n   prop first last 
##    <dbl> <chr> <chr>     <int>  <dbl> <chr> <chr>
##  1  1880 F     Mary       7065 0.0724 M     y    
##  2  1880 F     Anna       2604 0.0267 A     a    
##  3  1880 F     Emma       2003 0.0205 E     a    
##  4  1880 F     Elizabeth  1939 0.0199 E     h    
##  5  1880 F     Minnie     1746 0.0179 M     e    
##  6  1880 F     Margaret   1578 0.0162 M     t    
##  7  1880 F     Ida        1472 0.0151 I     a    
##  8  1880 F     Alice      1414 0.0145 A     e    
##  9  1880 F     Bertha     1320 0.0135 B     a    
## 10  1880 F     Sarah      1288 0.0132 S     h    
## # ℹ 1,924,655 more rows

Lange Strings

str_trunc(x, 30): kein String hat mehr Zeichen, als 30. Alles dahinter wird durch ... ersetzt.
str_wrap(x, 30) gibt einem String, der “zu lang” ist, eine neue Zeile.

x <- "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."

str_view(str_trunc(x, 30), "[aeiou]")
## [1] │ L<o>r<e>m <i>ps<u>m d<o>l<o>r s<i>t <a>m<e>t,...
str_view(str_wrap(x, 30), "[aeiou]")
## [1] │ L<o>r<e>m <i>ps<u>m d<o>l<o>r s<i>t <a>m<e>t,
##     │ c<o>ns<e>ct<e>t<u>r <a>d<i>p<i>sc<i>ng
##     │ <e>l<i>t, s<e>d d<o> <e><i><u>sm<o>d t<e>mp<o>r
##     │ <i>nc<i>d<i>d<u>nt <u>t l<a>b<o>r<e> <e>t d<o>l<o>r<e>
##     │ m<a>gn<a> <a>l<i>q<u><a>. Ut <e>n<i>m <a>d
##     │ m<i>n<i>m v<e>n<i><a>m, q<u><i>s n<o>str<u>d
##     │ <e>x<e>rc<i>t<a>t<i><o>n <u>ll<a>mc<o> l<a>b<o>r<i>s
##     │ n<i>s<i> <u>t <a>l<i>q<u><i>p <e>x <e><a> c<o>mm<o>d<o>
##     │ c<o>ns<e>q<u><a>t.

Reguläre Ausdrücke

Einleitung

In diesem Kapitel konzentrieren wir uns auf Funktionen, die Regular Expressions benutzen. Eine mächtige Sprache, um Muster innerhalb von Strings zu beschreiben.

Voraussetzungen

library(tidyverse)
library(babynames)

Muster: Basics

Das zweite Argument wird optisch in der Ausgabe hervorgehoben. Hier also z.B. blackberry durch blau und <…>.

str_view(fruit, "berry")
##  [6] │ bil<berry>
##  [7] │ black<berry>
## [10] │ blue<berry>
## [11] │ boysen<berry>
## [19] │ cloud<berry>
## [21] │ cran<berry>
## [29] │ elder<berry>
## [32] │ goji <berry>
## [33] │ goose<berry>
## [38] │ huckle<berry>
## [50] │ mul<berry>
## [70] │ rasp<berry>
## [73] │ salal <berry>
## [76] │ straw<berry>
#>  [6] │ bil<berry>
#>  [7] │ black<berry>
#> [10] │ blue<berry>
#> [11] │ boysen<berry>
#> [19] │ cloud<berry>
#> [21] │ cran<berry>
#> [29] │ elder<berry>
#> [32] │ goji <berry>
#> [33] │ goose<berry>
#> [38] │ huckle<berry>
#> ... and 4 more

a. matcht jeden String, der ein “a” enthält, gefolgt von weiterem Character.

str_view(c("a", "ab", "ae", "bd", "ea", "eab"), "a.")
## [2] │ <ab>
## [3] │ <ae>
## [6] │ e<ab>
#> [2] │ <ab>
#> [3] │ <ae>
#> [6] │ e<ab>

Wir können alle Früchte finden, die ein “a” enthalten, gefolgt von drei Buchstaben, gefolgt von einem “e”.

str_view(fruit, "a...e")
##  [1] │ <apple>
##  [7] │ bl<ackbe>rry
## [48] │ mand<arine>
## [51] │ nect<arine>
## [62] │ pine<apple>
## [64] │ pomegr<anate>
## [70] │ r<aspbe>rry
## [73] │ sal<al be>rry
#>  [1] │ <apple>
#>  [7] │ bl<ackbe>rry
#> [48] │ mand<arine>
#> [51] │ nect<arine>
#> [62] │ pine<apple>
#> [64] │ pomegr<anate>
#> [70] │ r<aspbe>rry
#> [73] │ sal<al be>rry

Quantifiers kontrollieren wie oft ein Muster zutrifft.

  • ?: Muster optional - trifft 0 oder 1 mal zu
  • +: Muster wiederholt sich - es trifft mind. einmal zu
  • *: Muster optional oder wiederholt sich
# ab? matches an "a", optionally followed by a "b".
str_view(c("a", "ab", "abb"), "ab?")
## [1] │ <a>
## [2] │ <ab>
## [3] │ <ab>b
#> [1] │ <a>
#> [2] │ <ab>
#> [3] │ <ab>b

# ab+ matches an "a", followed by at least one "b".
str_view(c("a", "ab", "abb"), "ab+")
## [2] │ <ab>
## [3] │ <abb>
#> [2] │ <ab>
#> [3] │ <abb>

# ab* matches an "a", followed by any number of "b"s.
str_view(c("a", "ab", "abb"), "ab*")
## [1] │ <a>
## [2] │ <ab>
## [3] │ <abb>
#> [1] │ <a>
#> [2] │ <ab>
#> [3] │ <abb>

Character classes sind durch [] definiert. Buchstaben darin enthalten matchen: [abcd] - “a”, “b”, “c”, “d”. Alles außer “a”, “b”, “c”, “d” matcht: [^abcd]. Finde Wörter mit 3 Vokalen bzw. 4 Konsonanten hintereinander.

str_view(words, "[aeiou][aeiou][aeiou]")
##  [79] │ b<eau>ty
## [565] │ obv<iou>s
## [644] │ prev<iou>s
## [670] │ q<uie>t
## [741] │ ser<iou>s
## [915] │ var<iou>s
#>  [79] │ b<eau>ty
#> [565] │ obv<iou>s
#> [644] │ prev<iou>s
#> [670] │ q<uie>t
#> [741] │ ser<iou>s
#> [915] │ var<iou>s
str_view(words, "[^aeiou][^aeiou][^aeiou][^aeiou]")
##  [45] │ a<pply>
## [198] │ cou<ntry>
## [424] │ indu<stry>
## [830] │ su<pply>
## [836] │ <syst>em
#>  [45] │ a<pply>
#> [198] │ cou<ntry>
#> [424] │ indu<stry>
#> [830] │ su<pply>
#> [836] │ <syst>em

Zwei Vokale gefolgt von mind. zwei Konsonanten:

str_view(words, "[aeiou][aeiou][^aeiou][^aeiou]+")
##   [6] │ acc<ount>
##  [21] │ ag<ainst>
##  [31] │ alr<eady>
##  [34] │ alth<ough>
##  [37] │ am<ount>
##  [46] │ app<oint>
##  [47] │ appr<oach>
##  [52] │ ar<ound>
##  [61] │ <auth>ority
##  [79] │ be<auty>
## [100] │ b<oard>
## [112] │ brill<iant>
## [117] │ b<uild>
## [139] │ ch<airm>an
## [158] │ cl<ient>
## [195] │ c<ould>
## [196] │ c<ounc>il
## [197] │ c<ount>
## [198] │ c<ountry>
## [199] │ c<ounty>
## ... and 52 more
#>  [6] │ acc<ount>
#> [21] │ ag<ainst>
#> [31] │ alr<eady>
#> [34] │ alth<ough>
#> [37] │ am<ount>
#> [46] │ app<oint>
#> [47] │ appr<oach>
#> [52] │ ar<ound>
#> [61] │ <auth>ority
#> [79] │ be<auty>
#> ... and 62 more

Alternation, | sucht zwischen Mustern. “apple”, “pear”, “banana”. Oder ein wiederholter Vokal.

str_view(fruit, "apple|pear|banana")
#>  [1] │ <apple>
#>  [4] │ <banana>
#> [59] │ <pear>
#> [62] │ pine<apple>
str_view(fruit, "aa|ee|ii|oo|uu")
#>  [9] │ bl<oo>d orange
#> [33] │ g<oo>seberry
#> [47] │ lych<ee>
#> [66] │ purple mangost<ee>n

Key Funktionen

Da die Basics sitzen, benutzen wir sie zusammen mit stringr und tidyr Funktionen: detect, count, replace und extract.

Detect Matches

str_detect() gibt einen logical vector, der TRUE ist aus, wenn das Muster ein Elemment des Character Vectors trifft, ansonsten FALSE.

str_detect(c("a", "b", "c"), "[aeiou]")
## [1]  TRUE FALSE FALSE

Da str_detect() einen logical vector ausgibt, passt es gut zu filter(). Der Code findet alle Namen, die ein kleines “x” enthalten.

babynames |> 
  filter(str_detect(name, "x")) |> 
  count(name, wt = n, sort = TRUE)
## # A tibble: 974 × 2
##    name            n
##    <chr>       <int>
##  1 Alexander  665492
##  2 Alexis     399551
##  3 Alex       278705
##  4 Alexandra  232223
##  5 Max        148787
##  6 Alexa      123032
##  7 Maxine     112261
##  8 Alexandria  97679
##  9 Maxwell     90486
## 10 Jaxon       71234
## # ℹ 964 more rows

str_detect() können wir auch mit summarize() nutzen: sum(str_detect(x, pattern)) gibt die Anzahl der Beobachtungen aus, die matchen und mit mean(...) die Anteile.

babynames |> 
  group_by(year) |> 
  summarize(prop_x = mean(str_detect(name, "x"))) |> 
  ggplot(aes(x = year, y = prop_x)) + 
  geom_line()

Zwei Funktionen sind eng verwandt mit str_detect(): str_subst(), das die treffenden Strings ausgibt und str_which(), das die treffenden Indices ausgibt.

str_subset(c("a", "b", "c"), "[aeiou]")
## [1] "a"
str_which(c("a", "b", "c"), "[aeiou]")
## [1] 1

Count Matches

str_count() sagt uns, wieviele Matches in jedem String sind.

x <- c("apple", "banana", "pear")
str_count(x, "p")
## [1] 2 0 1

Wieviele Vokale und Konsonante sind in jedem Namen?

babynames |> 
  count(name) |> 
  mutate(
    vowels = str_count(name, "[aeiou]"),
    consonants = str_count(name, "[^aeiou]")
  )
## # A tibble: 97,310 × 4
##    name          n vowels consonants
##    <chr>     <int>  <int>      <int>
##  1 Aaban        10      2          3
##  2 Aabha         5      2          3
##  3 Aabid         2      2          3
##  4 Aabir         1      2          3
##  5 Aabriella     5      4          5
##  6 Aada          1      2          2
##  7 Aadam        26      2          3
##  8 Aadan        11      2          3
##  9 Aadarsh      17      2          5
## 10 Aaden        18      2          3
## # ℹ 97,300 more rows

Regular Expressions sind case sensitive! Drei Möglichkeiten es zu beheben:

  • str_count(name, "[aeiouAEIOU]").
  • str_count(regex(name, ignore_case = TRUE), "[aeiou]") ignoriere Case.
  • nutze str_to_lower(), um die Namen in lower case zu konvertieren: str_count(str_to_lower(name), "[aeiou]").
babynames |> 
  count(name) |> 
  mutate(
    name = str_to_lower(name),
    vowels = str_count(name, "[aeiou]"),
    consonants = str_count(name, "[^aeiou]")
  )
## # A tibble: 97,310 × 4
##    name          n vowels consonants
##    <chr>     <int>  <int>      <int>
##  1 aaban        10      3          2
##  2 aabha         5      3          2
##  3 aabid         2      3          2
##  4 aabir         1      3          2
##  5 aabriella     5      5          4
##  6 aada          1      3          1
##  7 aadam        26      3          2
##  8 aadan        11      3          2
##  9 aadarsh      17      3          4
## 10 aaden        18      3          2
## # ℹ 97,300 more rows

Werte ersetzen

Wir können Matches modifizieren mit str_replace() und str_replace_all().

x <- c("apple", "pear", "banana")
str_replace_all(x, "[aeiou]", "-")
## [1] "-ppl-"  "p--r"   "b-n-n-"

str_remove() und str_remove_all() sind Abkürzungen für str_replace(x, pattern, "").

x <- c("apple", "pear", "banana")
str_remove_all(x, "[aeiou]")
## [1] "ppl" "pr"  "bnn"

Variablen extrahieren

Muster - Details

Escaping

# To create the regular expression \., we need to use \\.
dot <- "\\."

# But the expression itself only contains one \
str_view(dot)
## [1] │ \.
#> [1] │ \.

# And this tells R to look for an explicit .
str_view(c("abc", "a.c", "bef"), "a\\.c")
## [2] │ <a.c>
#> [2] │ <a.c>
x <- "a\\b"
str_view(x)
## [1] │ a\b
#> [1] │ a\b
str_view(x, "\\\\")
## [1] │ a<\>b
#> [1] │ a<\>b
str_view(x, r"{\\}")
## [1] │ a<\>b
#> [1] │ a<\>b

Wenn du versuchst ein Buchtabnesymbol (Literal) zu matchen (., $, |, *, +, ?, {, }, (, )), dann gibt es eine Alternative zur Nutzung des  Escape: [.], [$], …

str_view(c("abc", "a.c", "a*c", "a c"), "a[.]c")
## [2] │ <a.c>
#> [2] │ <a.c>
str_view(c("abc", "a.c", "a*c", "a c"), ".[*]c")
## [3] │ <a*c>
#> [3] │ <a*c>

Anker

Wenn du am Anfang matchen willst: ^, am Ende: $.

str_view(fruit, "^a")
## [1] │ <a>pple
## [2] │ <a>pricot
## [3] │ <a>vocado
#> [1] │ <a>pple
#> [2] │ <a>pricot
#> [3] │ <a>vocado
str_view(fruit, "a$")
##  [4] │ banan<a>
## [15] │ cherimoy<a>
## [30] │ feijo<a>
## [36] │ guav<a>
## [56] │ papay<a>
## [74] │ satsum<a>
#>  [4] │ banan<a>
#> [15] │ cherimoy<a>
#> [30] │ feijo<a>
#> [36] │ guav<a>
#> [56] │ papay<a>
#> [74] │ satsum<a>

Full String mit ^ und $:

str_view(fruit, "apple")
##  [1] │ <apple>
## [62] │ pine<apple>
#>  [1] │ <apple>
#> [62] │ pine<apple>
str_view(fruit, "^apple$")
## [1] │ <apple>
#> [1] │ <apple>

Matche die Grenze zwischen zwei Wörtern (Start, Ende) mit \b. Finde so alle Nutzungen von sum(). Suche nach \bsum\b, um summarize, rowsum oder ähnliches zu vermeiden.

x <- c("summary(x)", "summarize(df)", "rowsum(x)", "sum(x)")
str_view(x, "sum")
## [1] │ <sum>mary(x)
## [2] │ <sum>marize(df)
## [3] │ row<sum>(x)
## [4] │ <sum>(x)
#> [1] │ <sum>mary(x)
#> [2] │ <sum>marize(df)
#> [3] │ row<sum>(x)
#> [4] │ <sum>(x)
str_view(x, "\\bsum\\b")
## [4] │ <sum>(x)
#> [4] │ <sum>(x)
str_view("abc", c("$", "^", "\\b"))
## [1] │ abc<>
## [2] │ <>abc
## [3] │ <>abc<>
#> [1] │ abc<>
#> [2] │ <>abc
#> [3] │ <>abc<>
str_replace_all("abc", c("$", "^", "\\b"), "--")
## [1] "abc--"   "--abc"   "--abc--"

Character - Klassen

Du kannst deine eigenen Mengen mit [] konstruieren, wo [abc] a, b, oder c matcht. Es gibt drei Character, die innerhalb der eckigen Klammern eine spezielle Bedeutung haben:

  • eine Spanne wird definiert [a-z], die Kleinbuchstaben matcht und [0-9] Zahlen.
  • ^ Inverse nimmt ales bis auf a, b oder c auf: [^abc].
  • \ trennt Zeichen, so dass ^ oder - oder ] matcht: [\^\-\]].
x <- "abcd ABCD 12345 -!@#%."
str_view(x, "[abc]+")
## [1] │ <abc>d ABCD 12345 -!@#%.
#> [1] │ <abc>d ABCD 12345 -!@#%.
str_view(x, "[a-z]+")
## [1] │ <abcd> ABCD 12345 -!@#%.
#> [1] │ <abcd> ABCD 12345 -!@#%.
str_view(x, "[^a-z0-9]+")
## [1] │ abcd< ABCD >12345< -!@#%.>
#> [1] │ abcd< ABCD >12345< -!@#%.>

# You need an escape to match characters that are otherwise
# special inside of []
str_view("a-b-c", "[a-c]")
## [1] │ <a>-<b>-<c>
#> [1] │ <a>-<b>-<c>
str_view("a-b-c", "[a\\-c]")
## [1] │ <a><->b<-><c>
#> [1] │ <a><->b<-><c>

Factors - Faktoren

Einleitung

Faktoren werden für kategoriale Variablen genutzt; Variablen, die eine feste Menge an möglichen Werten haben.

Voraussetzungen

Das forcats Paket ist Teil von tidyverse.

library(tidyverse)

Faktoren - Basics

x1 <- c("Dec", "Apr", "Jan", "Mar")

Als String diese Werte aufzunehmen, hat zwei Probleme:

  1. Tippfehler
x2 <- c("Dec", "Apr", "Jam", "Mar")
  1. Sortieren
sort(x1)
## [1] "Apr" "Dec" "Jan" "Mar"

Um einen Faktor zu kreieren, starte mit einer Liste valider levels:

month_levels <- c(
  "Jan", "Feb", "Mar", "Apr", "May", "Jun", 
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
)

Daraus kannst du einen Faktor bauen:

y1 <- factor(x1, levels = month_levels)
y1
## [1] Dec Apr Jan Mar
## Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
sort(y1)
## [1] Jan Mar Apr Dec
## Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

Jeder Wert, der sich nicht in levels befindet, wird zu NA konvertiert.

y2 <- factor(x2, levels = month_levels)
y2
## [1] Dec  Apr  <NA> Mar 
## Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

In alphabetischer Reihenfolge werden die Daten genommen, wenn du die Levels vergisst.

factor(x1)
## [1] Dec Apr Jan Mar
## Levels: Apr Dec Jan Mar

Du kannst die Reihenfolge nach dem erstmaligen Erscheinen festlegen:

f1 <- factor(x1, levels = unique(x1))
f1
## [1] Dec Apr Jan Mar
## Levels: Dec Apr Jan Mar
f2 <- x1 |> factor() |> fct_inorder()
f2
## [1] Dec Apr Jan Mar
## Levels: Dec Apr Jan Mar

Beim Einlesen der Daten kannst du direkt einen Faktor erzeugen mit col_factor():

library(readr)
month_levels <- c(
  "Jan", "Feb", "Mar", "Apr", "May", "Jun", 
  "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
)

csv <- "
month,value
Jan,12
Feb,56
Mar,12"

df <- read_csv(csv, col_types = cols(month = col_factor(month_levels)))
df$month
#> [1] Jan Feb Mar
#> Levels: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

General Social Survey

Wir benutzen: forcats::gss_cat. Der Survey hat Tausende von Fragen.

gss_cat
## # A tibble: 21,483 × 9
##     year marital         age race  rincome        partyid    relig denom tvhours
##    <int> <fct>         <int> <fct> <fct>          <fct>      <fct> <fct>   <int>
##  1  2000 Never married    26 White $8000 to 9999  Ind,near … Prot… Sout…      12
##  2  2000 Divorced         48 White $8000 to 9999  Not str r… Prot… Bapt…      NA
##  3  2000 Widowed          67 White Not applicable Independe… Prot… No d…       2
##  4  2000 Never married    39 White Not applicable Ind,near … Orth… Not …       4
##  5  2000 Divorced         25 White Not applicable Not str d… None  Not …       1
##  6  2000 Married          25 White $20000 - 24999 Strong de… Prot… Sout…      NA
##  7  2000 Never married    36 White $25000 or more Not str r… Chri… Not …       3
##  8  2000 Divorced         44 White $7000 to 7999  Ind,near … Prot… Luth…      NA
##  9  2000 Married          44 White $25000 or more Not str d… Prot… Other       0
## 10  2000 Married          47 White $25000 or more Strong re… Prot… Sout…       3
## # ℹ 21,473 more rows

In einem tibble kannst du die Level mit count() sehen.

gss_cat |>
  count(race)
## # A tibble: 3 × 2
##   race      n
##   <fct> <int>
## 1 Other  1959
## 2 Black  3129
## 3 White 16395

Oder mit einem Säulendiagramm:

ggplot(gss_cat, aes(x = race)) +
  geom_bar()

Reihenfolge der factors ändern

Manchmal macht es Sinn die Reihenfolge der Faktoren zu ändern. Schauen wir uns die durchschnittliche TV Zeit pro Tag und Religionen an.

relig_summary <- gss_cat |>
  group_by(relig) |>
  summarize(
    age = mean(age, na.rm = TRUE),
    tvhours = mean(tvhours, na.rm = TRUE),
    n = n()
  )

ggplot(relig_summary, aes(x = tvhours, y = relig)) + 
  geom_point()

Der Plot ist schwer zu lesen, da es hier kein Muster gibt. fct_reorder() sortiert um, und nimmt dabei drei Argumente an:

  • f, der Faktor, wessen Levels du modifizieren willst.
  • x, ein numerischer Vektor, den du benutzt, um die Reihenfolge zu bestimmen.
  • optional: fun, eine Funktion, wenn es multiple Werte von x gibt, für jeden Wert von f. Default ist Median.
ggplot(relig_summary, aes(x = tvhours, y = fct_reorder(relig, tvhours))) +
  geom_point()

Bei komplizierten Transformationen empfehlen wir sie aus aes() zu nehmen und in ein separates mutate() zu stecken.

relig_summary |>
  mutate(
    relig = fct_reorder(relig, tvhours)
  ) |>
  ggplot(aes(x = tvhours, y = relig)) +
  geom_point()

Wie sieht es mit der Altersverteilung in bestimmten Einkommensstufen aus?

rincome_summary <- gss_cat |>
  group_by(rincome) |>
  summarize(
    age = mean(age, na.rm = TRUE),
    tvhours = mean(tvhours, na.rm = TRUE),
    n = n()
  )
rincome_summary
## # A tibble: 16 × 4
##    rincome          age tvhours     n
##    <fct>          <dbl>   <dbl> <int>
##  1 No answer       45.5    2.90   183
##  2 Don't know      45.6    3.41   267
##  3 Refused         47.6    2.48   975
##  4 $25000 or more  44.2    2.23  7363
##  5 $20000 - 24999  41.5    2.78  1283
##  6 $15000 - 19999  40.0    2.91  1048
##  7 $10000 - 14999  41.1    3.02  1168
##  8 $8000 to 9999   41.1    3.15   340
##  9 $7000 to 7999   38.2    2.65   188
## 10 $6000 to 6999   40.3    3.17   215
## 11 $5000 to 5999   37.8    3.16   227
## 12 $4000 to 4999   38.9    3.15   226
## 13 $3000 to 3999   37.8    3.31   276
## 14 $1000 to 2999   34.5    3.00   395
## 15 Lt $1000        40.5    3.36   286
## 16 Not applicable  56.1    3.79  7043

Und dann bitte nett darstellen.

ggplot(rincome_summary, aes(x = age, y = fct_reorder(rincome, age))) + 
  geom_point()

Die Reihenfolge hier sollte jedoch noch einmal überdacht werden. Am Anfang sollte jedoch “Not applicable” stehen. Nutze fct_relevel(). Es nimmt einen Faktor f, dann jede Anzahl an Levels, die du in die erste Zeile(n) schreiben willst.

ggplot(rincome_summary, aes(x = age, y = fct_relevel(rincome, "Not applicable"))) +
  geom_point()

Ein anderer Typ der Umordnung ist nützlich, wenn du die Linien eines Plots farblich einfärbst. Durch die Umsortierung passen sich die Farben des Plots auf der rechten Seite der Legende an.

by_age <- gss_cat |>
  filter(!is.na(age)) |>
  count(age, marital) |>
  group_by(age) |>
  mutate(
    prop = n / sum(n)
  )

ggplot(by_age, aes(x = age, y = prop, color = marital)) +
  geom_line(na.rm = TRUE)

ggplot(by_age, aes(x = age, y = prop, color = fct_reorder2(marital, age, prop))) +
  geom_line() +
  labs(color = "marital")

Für Balkendiagramme kannst du fct_infreq() benutzen, um Levels Nach Häufigkeit zu sortieren. Kombiniere es mit fct_rev(), wenn du den häufigsten Wert auf der rechten, statt auf der linken Seite haben willst.

gss_cat |>
  mutate(marital = marital |> fct_infreq() |> fct_rev()) |>
  ggplot(aes(x = marital)) +
  geom_bar()

Faktorlevel modifizieren

Mächtiger als die Reihenfolge zu verändern ist es, ihre Werte zu wechseln. Das mächtigste Werkzeug ist fct_recode(). Es wechselt den Wert von jedem Level.

gss_cat |> count(partyid)
## # A tibble: 10 × 2
##    partyid                n
##    <fct>              <int>
##  1 No answer            154
##  2 Don't know             1
##  3 Other party          393
##  4 Strong republican   2314
##  5 Not str republican  3032
##  6 Ind,near rep        1791
##  7 Independent         4119
##  8 Ind,near dem        2499
##  9 Not str democrat    3690
## 10 Strong democrat     3490

Die Level sind lapidar und inkonsistent. Die neuen Werte stehen auf der linken und die alten Werte auf der rechten Seite.

gss_cat |>
  mutate(
    partyid = fct_recode(partyid,
      "Republican, strong"    = "Strong republican",
      "Republican, weak"      = "Not str republican",
      "Independent, near rep" = "Ind,near rep",
      "Independent, near dem" = "Ind,near dem",
      "Democrat, weak"        = "Not str democrat",
      "Democrat, strong"      = "Strong democrat"
    )
  ) |>
  count(partyid)
## # A tibble: 10 × 2
##    partyid                   n
##    <fct>                 <int>
##  1 No answer               154
##  2 Don't know                1
##  3 Other party             393
##  4 Republican, strong     2314
##  5 Republican, weak       3032
##  6 Independent, near rep  1791
##  7 Independent            4119
##  8 Independent, near dem  2499
##  9 Democrat, weak         3690
## 10 Democrat, strong       3490

Um Gruppen zu kombinieren, kannst du einfach verschiedenen alten Level, gleiche neue verpassen.

gss_cat |>
  mutate(
    partyid = fct_recode(partyid,
      "Republican, strong"    = "Strong republican",
      "Republican, weak"      = "Not str republican",
      "Independent, near rep" = "Ind,near rep",
      "Independent, near dem" = "Ind,near dem",
      "Democrat, weak"        = "Not str democrat",
      "Democrat, strong"      = "Strong democrat",
      "Other"                 = "No answer",
      "Other"                 = "Don't know",
      "Other"                 = "Other party"
    )
  ) |>
  count(partyid)
## # A tibble: 8 × 2
##   partyid                   n
##   <fct>                 <int>
## 1 Other                   548
## 2 Republican, strong     2314
## 3 Republican, weak       3032
## 4 Independent, near rep  1791
## 5 Independent            4119
## 6 Independent, near dem  2499
## 7 Democrat, weak         3690
## 8 Democrat, strong       3490

fct_collapse() ist eine Variante, wenn du sehr viele Level zusammenfassen willst.

gss_cat |>
  mutate(
    partyid = fct_collapse(partyid,
      "other" = c("No answer", "Don't know", "Other party"),
      "rep" = c("Strong republican", "Not str republican"),
      "ind" = c("Ind,near rep", "Independent", "Ind,near dem"),
      "dem" = c("Not str democrat", "Strong democrat")
    )
  ) |>
  count(partyid)
## # A tibble: 4 × 2
##   partyid     n
##   <fct>   <int>
## 1 other     548
## 2 rep      5346
## 3 ind      8409
## 4 dem      7180

Manchmal willst du kleine Gruppen zusammenwerfen. Diesen Job macht fct_lump_*(). fct_lump_lowfreq() schmeißt die kleinsten Gruppen in eine “Other” Kategorie.

gss_cat |>
  mutate(relig = fct_lump_lowfreq(relig)) |>
  count(relig)
## # A tibble: 2 × 2
##   relig          n
##   <fct>      <int>
## 1 Protestant 10846
## 2 Other      10637

Ein bisschen mehr Details wollen wir schon sehen! Wir wollen genau n=10 Gruppen sehen, mit fct_lump_n().

gss_cat |>
  mutate(relig = fct_lump_n(relig, n = 10)) |>
  count(relig, sort = TRUE) |>
  print(n = Inf)
## # A tibble: 10 × 2
##    relig                       n
##    <fct>                   <int>
##  1 Protestant              10846
##  2 Catholic                 5124
##  3 None                     3523
##  4 Christian                 689
##  5 Other                     458
##  6 Jewish                    388
##  7 Buddhism                  147
##  8 Inter-nondenominational   109
##  9 Moslem/islam              104
## 10 Orthodox-christian         95

Dates und Zeiten

Einleitung

Voraussetzungen

Wir konzentrieren uns auf das lubridate Paket.

library(tidyverse)
library(lubridate)
library(nycflights13)

Kreieren von date/times

Drei Typen von date/time beziehen sich auf den Moment der Zeit:

date - Tibble als <date>
time - Tibble als <time>
date-time - Tibble als <dttm>

Für das aktuelle Datum oder date-time, nutze today() oder now():

today()
## [1] "2023-10-06"
now()
## [1] "2023-10-06 06:28:06 CEST"

Die folgenden Abschnitte beschreiben vier weitere Möglichkeiten.

Während Import mit readr

csv <- "
  date,datetime
  2022-01-02,2022-01-02 05:12
"
read_csv(csv)
## Rows: 1 Columns: 2
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## dttm (1): datetime
## date (1): date
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
## # A tibble: 1 × 2
##   date       datetime           
##   <date>     <dttm>             
## 1 2022-01-02 2022-01-02 05:12:00

Das Ausgabeformat kannst du individuell bestimmen.

csv <- "
  date
  01/02/15
"

read_csv(csv, col_types = cols(date = col_date("%m/%d/%y")))
## # A tibble: 1 × 1
##   date      
##   <date>    
## 1 2015-01-02
read_csv(csv, col_types = cols(date = col_date("%d/%m/%y")))
## # A tibble: 1 × 1
##   date      
##   <date>    
## 1 2015-02-01
read_csv(csv, col_types = cols(date = col_date("%y/%m/%d")))
## # A tibble: 1 × 1
##   date      
##   <date>    
## 1 2001-02-15

Von Strings

ymd("2017-01-31")
## [1] "2017-01-31"
mdy("January 31st, 2017")
## [1] "2017-01-31"
dmy("31-Jan-2017")
## [1] "2017-01-31"

Um ein date-time zu kreieren, füge _ und mind. “h”, “m” oder “s” hinzu:

ymd_hms("2017-01-31 20:11:59")
#> [1] "2017-01-31 20:11:59 UTC"
mdy_hm("01/31/2017 08:01")
#> [1] "2017-01-31 08:01:00 UTC"

Von individuellen Komponenten

flights |> 
  select(year, month, day, hour, minute)
## # A tibble: 336,776 × 5
##     year month   day  hour minute
##    <int> <int> <int> <dbl>  <dbl>
##  1  2013     1     1     5     15
##  2  2013     1     1     5     29
##  3  2013     1     1     5     40
##  4  2013     1     1     5     45
##  5  2013     1     1     6      0
##  6  2013     1     1     5     58
##  7  2013     1     1     6      0
##  8  2013     1     1     6      0
##  9  2013     1     1     6      0
## 10  2013     1     1     6      0
## # ℹ 336,766 more rows
flights |> 
  select(year, month, day, hour, minute) |> 
  mutate(departure = make_datetime(year, month, day, hour, minute))
## # A tibble: 336,776 × 6
##     year month   day  hour minute departure          
##    <int> <int> <int> <dbl>  <dbl> <dttm>             
##  1  2013     1     1     5     15 2013-01-01 05:15:00
##  2  2013     1     1     5     29 2013-01-01 05:29:00
##  3  2013     1     1     5     40 2013-01-01 05:40:00
##  4  2013     1     1     5     45 2013-01-01 05:45:00
##  5  2013     1     1     6      0 2013-01-01 06:00:00
##  6  2013     1     1     5     58 2013-01-01 05:58:00
##  7  2013     1     1     6      0 2013-01-01 06:00:00
##  8  2013     1     1     6      0 2013-01-01 06:00:00
##  9  2013     1     1     6      0 2013-01-01 06:00:00
## 10  2013     1     1     6      0 2013-01-01 06:00:00
## # ℹ 336,766 more rows

Missing Values

Einleitung

Ein paar Basics haben wir schon kennengelernt, jetzt gehen wir ins Detail.

Voraussetzungen

Die meisten Funktionen kommen von dplyr und tidyr, die Teil des tidyverse sind.

library(tidyverse)

Eindeutig Missing Values

Ein paar handliche Werkzeuge, um Missing Values zu kreieren oder zu eliminieren. Zellen mit NA.

Last Obversation carried forward

Manchmal zeigt ein NA, dass der Wert der vorangehenden Zeile wiederholt wurde.

treatment <- tribble(
  ~person,           ~treatment, ~response,
  "Derrick Whitmore", 1,         7,
  NA,                 2,         10,
  NA,                 3,         NA,
  "Katherine Burke",  1,         4
)

Diese NA kann man füllen mit tidyr::fill(). Es nimmt eine Menge von Spalten auf.

treatment |>
  fill(everything())
## # A tibble: 4 × 3
##   person           treatment response
##   <chr>                <dbl>    <dbl>
## 1 Derrick Whitmore         1        7
## 2 Derrick Whitmore         2       10
## 3 Derrick Whitmore         3       10
## 4 Katherine Burke          1        4

Feste Werte

Manchmal werden NA’s ersetzt durch feste, bekannte Werte wie 0. Benutze dplyr:: coalesce(), um die NA’s zu ersetzen:

x <- c(1, 4, 5, 7, NA)
coalesce(x, 0)
## [1] 1 4 5 7 0

Oft wird eine Zahl wie die -99 als NA geschrieben. Ersetze sie durch NA mit der Funktion dplyr::na_if():

x <- c(1, 4, 5, 7, -99)
na_if(x, -99)
## [1]  1  4  5  7 NA

NaN

Ein spezieller Typ von Missing Values ist NaN, er verhält sich wie NA:

x <- c(NA, NaN)
x * 10
## [1]  NA NaN
x == 1
## [1] NA NA
is.na(x)
## [1] TRUE TRUE

NaN trifft häufig bei mathematischen Operationen auf:

0 / 0 
## [1] NaN
0 * Inf
## [1] NaN
Inf - Inf
## [1] NaN

Implizierte Missing Values

Explizite Missing Values kannst du durch ein NA lokalisieren. Implizierte Missing Values zeichnen sich dadurch aus, dass ganze Zeilen oder Spalten fehlen.

stocks <- tibble(
  year  = c(2020, 2020, 2020, 2020, 2021, 2021, 2021),
  qtr   = c(   1,    2,    3,    4,    2,    3,    4),
  price = c(1.88, 0.59, 0.35,   NA, 0.92, 0.17, 2.66)
)

Pivoting

Ein Werkzeug kann implizierte Missings explizit machen, und umgekehrt: Pivoting. In unserem Beispiel muss jede Kombination von Zeilen und Spalten einen Wert haben.

stocks |>
  pivot_wider(
    names_from = qtr, 
    values_from = price
  )
## # A tibble: 2 × 5
##    year   `1`   `2`   `3`   `4`
##   <dbl> <dbl> <dbl> <dbl> <dbl>
## 1  2020  1.88  0.59  0.35 NA   
## 2  2021 NA     0.92  0.17  2.66

Complete

tidyr::complete() generiert explizite Missings, durch das Anbieten der Variablen, die die Kombinationen von Zeilen definieren, die existieren sollten. Zum Beispiel wissen wir, dass alle Kombinationen von year und qtr in stocks existieren sollten.

stocks |>
  complete(year, qtr)
## # A tibble: 8 × 3
##    year   qtr price
##   <dbl> <dbl> <dbl>
## 1  2020     1  1.88
## 2  2020     2  0.59
## 3  2020     3  0.35
## 4  2020     4 NA   
## 5  2021     1 NA   
## 6  2021     2  0.92
## 7  2021     3  0.17
## 8  2021     4  2.66

Du rufst complete() mit den existierenden Variablen auf, so dass die fehlenden Kombinationen aufgefüllt werden. Sind die Variablen selbst nicht komplett, kannst du eigene Werte liefern. So kannst du den stocks Datensatz von 2019 bis 2021 laufen lassen.

stocks |>
  complete(year = 2019:2021, qtr)
## # A tibble: 12 × 3
##     year   qtr price
##    <dbl> <dbl> <dbl>
##  1  2019     1 NA   
##  2  2019     2 NA   
##  3  2019     3 NA   
##  4  2019     4 NA   
##  5  2020     1  1.88
##  6  2020     2  0.59
##  7  2020     3  0.35
##  8  2020     4 NA   
##  9  2021     1 NA   
## 10  2021     2  0.92
## 11  2021     3  0.17
## 12  2021     4  2.66

Benutze full_seq(x, 1), um fehlende Lücken zu stopfen, hier in Einer-Schritten.

full_seq(c(1, 2, 4, 5, 10), 1)
##  [1]  1  2  3  4  5  6  7  8  9 10

Joins

Joins lernen wir später näher kennen. dplyr::anti_join(x, y) nimmt nur die Reihen in x, die kein Match in y haben. So können wir enthüllen, dass Informationen zu vier Flughäfen fehlen, die in flights genannt wurden.

library(nycflights13)

flights |> 
  distinct(faa = dest) |> 
  anti_join(airports)
## Joining with `by = join_by(faa)`
## # A tibble: 4 × 1
##   faa  
##   <chr>
## 1 BQN  
## 2 SJU  
## 3 STT  
## 4 PSE

Zuerst werden alle verschiedenen dest in faa gespeichert. Mit anti_join(airports) wird dann in der Variable faa in dem Datensatz airports geschaut, ob die dest dort zu finden sind. In unserem Fall fehlen dort vier Flughäfen.

flights |> 
  distinct(tailnum) |> 
  anti_join(planes)
## Joining with `by = join_by(tailnum)`
## # A tibble: 722 × 1
##    tailnum
##    <chr>  
##  1 N3ALAA 
##  2 N3DUAA 
##  3 N542MQ 
##  4 N730MQ 
##  5 N9EAMQ 
##  6 N532UA 
##  7 N3EMAA 
##  8 N518MQ 
##  9 N3BAAA 
## 10 N3CYAA 
## # ℹ 712 more rows

Faktoren und leere Gruppen

health <- tibble(
  name   = c("Ikaia", "Oletta", "Leriah", "Dashay", "Tresaun"),
  smoker = factor(c("no", "no", "no", "no", "no"), levels = c("yes", "no")),
  age    = c(34L, 88L, 75L, 47L, 56L),
)

Wir wollen erstmal die Anzahl an Rauchern zählen mit dplyr::count().

health |> count(smoker)
## # A tibble: 1 × 2
##   smoker     n
##   <fct>  <int>
## 1 no         5

Da die Gruppe nur Nichtraucher enthält, werden keine Nicht-Raucher angezeigt. Wir können aber nach diesen nachfragen, auch wenn keine vorhanden sind, mit .drop = FALSE:

health |> count(smoker, .drop = FALSE)
## # A tibble: 2 × 2
##   smoker     n
##   <fct>  <int>
## 1 yes        0
## 2 no         5

Gleiches mit Achsen:

ggplot(health, aes(x = smoker)) +
  geom_bar() +
  scale_x_discrete()

ggplot(health, aes(x = smoker)) +
  geom_bar() +
  scale_x_discrete(drop = FALSE)

Gleiches Problem mit dplyr::group_by():

health |> 
  group_by(smoker, .drop = FALSE) |> 
  summarize(
    n = n(),
    mean_age = mean(age),
    min_age = min(age),
    max_age = max(age),
    sd_age = sd(age)
  )
## Warning: There were 2 warnings in `summarize()`.
## The first warning was:
## ℹ In argument: `min_age = min(age)`.
## ℹ In group 1: `smoker = yes`.
## Caused by warning in `min()`:
## ! kein nicht-fehlendes Argument für min; gebe Inf zurück
## ℹ Run `dplyr::last_dplyr_warnings()` to see the 1 remaining warning.
## # A tibble: 2 × 6
##   smoker     n mean_age min_age max_age sd_age
##   <fct>  <int>    <dbl>   <dbl>   <dbl>  <dbl>
## 1 yes        0      NaN     Inf    -Inf   NA  
## 2 no         5       60      34      88   21.6

Ein leerer Vektor hat die Länge 0, Missing Values haben jeweils die Länge 1.
Führe Summary durch und dann mache die implizierten Missings explizit mit complete().

health |> 
  group_by(smoker) |> 
  summarize(
    n = n(),
    mean_age = mean(age),
    min_age = min(age),
    max_age = max(age),
    sd_age = sd(age)
  ) |> 
  complete(smoker)
## # A tibble: 2 × 6
##   smoker     n mean_age min_age max_age sd_age
##   <fct>  <int>    <dbl>   <int>   <int>  <dbl>
## 1 yes       NA       NA      NA      NA   NA  
## 2 no         5       60      34      88   21.6

Joins

Einleitung

Es ist selten, dass du nur einen einzigen Data Frame hast. Normalerweise hast du mehrere und du musst sie “joinen”, also zusammenfügen. Es gibt zwei Arten von joins:

  • Mutating Joins - fügt neue Variablen einem Data Frame hinzu, von matching Observations in einem anderen.
  • Filtering Joins - filtert Beobachtungen eines Data Frames, je nachdem, ob sie eine Observation in einem anderen matchen.

Voraussetzungen

library(tidyverse)
library(nycflights13)

Schlüssel - Keys

Zuerst müssen wir verstehen, wie zwei Tabellen miteinander verbunden werden können. Dies durch ein Paar von Schlüsseln, die sich in jeder Tabelle befinden.

Primärschlüssel und Fremdschlüssel

Jedes “Join” involviert ein Paar von Schlüsseln: einen Primärschlüssel und einen Fremdschlüssel. Ein Primärschlüssel ist eine Variable, die jede Beobachtung eindeutig identifiziert. compound key sind mehr als eine Variable.

In airlines finden wir zwei Variablen zu jeder Airline: carrier code und name. Identifizieren lässt sich jede Airline eindeutig durch den carrier code, so dass carrier der Primärschlüssel ist.

airlines
## # A tibble: 16 × 2
##    carrier name                       
##    <chr>   <chr>                      
##  1 9E      Endeavor Air Inc.          
##  2 AA      American Airlines Inc.     
##  3 AS      Alaska Airlines Inc.       
##  4 B6      JetBlue Airways            
##  5 DL      Delta Air Lines Inc.       
##  6 EV      ExpressJet Airlines Inc.   
##  7 F9      Frontier Airlines Inc.     
##  8 FL      AirTran Airways Corporation
##  9 HA      Hawaiian Airlines Inc.     
## 10 MQ      Envoy Air                  
## 11 OO      SkyWest Airlines Inc.      
## 12 UA      United Air Lines Inc.      
## 13 US      US Airways Inc.            
## 14 VX      Virgin America             
## 15 WN      Southwest Airlines Co.     
## 16 YV      Mesa Airlines Inc.

In airports kann jeder Flughafen durch seinen airport code identifiziert werden: faa.

airports
## # A tibble: 1,458 × 8
##    faa   name                             lat    lon   alt    tz dst   tzone    
##    <chr> <chr>                          <dbl>  <dbl> <dbl> <dbl> <chr> <chr>    
##  1 04G   Lansdowne Airport               41.1  -80.6  1044    -5 A     America/…
##  2 06A   Moton Field Municipal Airport   32.5  -85.7   264    -6 A     America/…
##  3 06C   Schaumburg Regional             42.0  -88.1   801    -6 A     America/…
##  4 06N   Randall Airport                 41.4  -74.4   523    -5 A     America/…
##  5 09J   Jekyll Island Airport           31.1  -81.4    11    -5 A     America/…
##  6 0A9   Elizabethton Municipal Airport  36.4  -82.2  1593    -5 A     America/…
##  7 0G6   Williams County Airport         41.5  -84.5   730    -5 A     America/…
##  8 0G7   Finger Lakes Regional Airport   42.9  -76.8   492    -5 A     America/…
##  9 0P2   Shoestring Aviation Airfield    39.8  -76.6  1000    -5 U     America/…
## 10 0S9   Jefferson County Intl           48.1 -123.    108    -8 A     America/…
## # ℹ 1,448 more rows

In planes finden wir Infos zu jedem Flugzeug, das durch tailnum eindeutig identifiziert werden kann.

planes
## # A tibble: 3,322 × 9
##    tailnum  year type              manufacturer model engines seats speed engine
##    <chr>   <int> <chr>             <chr>        <chr>   <int> <int> <int> <chr> 
##  1 N10156   2004 Fixed wing multi… EMBRAER      EMB-…       2    55    NA Turbo…
##  2 N102UW   1998 Fixed wing multi… AIRBUS INDU… A320…       2   182    NA Turbo…
##  3 N103US   1999 Fixed wing multi… AIRBUS INDU… A320…       2   182    NA Turbo…
##  4 N104UW   1999 Fixed wing multi… AIRBUS INDU… A320…       2   182    NA Turbo…
##  5 N10575   2002 Fixed wing multi… EMBRAER      EMB-…       2    55    NA Turbo…
##  6 N105UW   1999 Fixed wing multi… AIRBUS INDU… A320…       2   182    NA Turbo…
##  7 N107US   1999 Fixed wing multi… AIRBUS INDU… A320…       2   182    NA Turbo…
##  8 N108UW   1999 Fixed wing multi… AIRBUS INDU… A320…       2   182    NA Turbo…
##  9 N109UW   1999 Fixed wing multi… AIRBUS INDU… A320…       2   182    NA Turbo…
## 10 N110UW   1999 Fixed wing multi… AIRBUS INDU… A320…       2   182    NA Turbo…
## # ℹ 3,312 more rows

weather liefert Daten zum Wetter an den Flughäfen. origin und time_hour sind also hier der compound Primärschlüssel.

weather
## # A tibble: 26,115 × 15
##    origin  year month   day  hour  temp  dewp humid wind_dir wind_speed
##    <chr>  <int> <int> <int> <int> <dbl> <dbl> <dbl>    <dbl>      <dbl>
##  1 EWR     2013     1     1     1  39.0  26.1  59.4      270      10.4 
##  2 EWR     2013     1     1     2  39.0  27.0  61.6      250       8.06
##  3 EWR     2013     1     1     3  39.0  28.0  64.4      240      11.5 
##  4 EWR     2013     1     1     4  39.9  28.0  62.2      250      12.7 
##  5 EWR     2013     1     1     5  39.0  28.0  64.4      260      12.7 
##  6 EWR     2013     1     1     6  37.9  28.0  67.2      240      11.5 
##  7 EWR     2013     1     1     7  39.0  28.0  64.4      240      15.0 
##  8 EWR     2013     1     1     8  39.9  28.0  62.2      250      10.4 
##  9 EWR     2013     1     1     9  39.9  28.0  62.2      260      15.0 
## 10 EWR     2013     1     1    10  41    28.0  59.6      260      13.8 
## # ℹ 26,105 more rows
## # ℹ 5 more variables: wind_gust <dbl>, precip <dbl>, pressure <dbl>,
## #   visib <dbl>, time_hour <dttm>

Ein Fremdschlüssel ist eine Variable (oder mehrere), die zu mit einem anderen Primärschlüssel in einer anderen Tabelle korrespondiert:

  • flights$tailnum ist Fremdschlüssel, der mit Primärschlüssel planes$tailnum korrespondiert.

  • flights$carrier zu airlines$carrier.

  • flights$origin zu airports$faa.

  • flights$dest zu airports$faa.

  • flights$origin-flights$time_hour compound Fremdschlüssel, der mit compound Primärschlüssel weather$origin-weather$time_hour korrespondiert.

Verbindung zwischen allen 5 Data Frames im nycflights13 Paket. PK grau, FK weiß.
Verbindung zwischen allen 5 Data Frames im nycflights13 Paket. PK grau, FK weiß.

Primärschlüssel checken

Sind sie wirklich einmalig, also die Beobachtungen?

planes |> 
  count(tailnum) |> 
  filter(n > 1)
## # A tibble: 0 × 2
## # ℹ 2 variables: tailnum <chr>, n <int>
weather |> 
  count(time_hour, origin) |> 
  filter(n > 1)
## # A tibble: 0 × 3
## # ℹ 3 variables: time_hour <dttm>, origin <chr>, n <int>

Checke auch nach Missing Values!

planes |> 
  filter(is.na(tailnum))
## # A tibble: 0 × 9
## # ℹ 9 variables: tailnum <chr>, year <int>, type <chr>, manufacturer <chr>,
## #   model <chr>, engines <int>, seats <int>, speed <int>, engine <chr>
weather |> 
  filter(is.na(time_hour) | is.na(origin))
## # A tibble: 0 × 15
## # ℹ 15 variables: origin <chr>, year <int>, month <int>, day <int>, hour <int>,
## #   temp <dbl>, dewp <dbl>, humid <dbl>, wind_dir <dbl>, wind_speed <dbl>,
## #   wind_gust <dbl>, precip <dbl>, pressure <dbl>, visib <dbl>,
## #   time_hour <dttm>

Surrogat-Schlüssel

Welches ist eigentlich der Primärschlüssel für flights? Es gibt drei Variablen, die zusammen jeden Flug eindeutig identifizieren:

flights |> 
  count(time_hour, carrier, flight) |> 
  filter(n > 1)
## # A tibble: 0 × 4
## # ℹ 4 variables: time_hour <dttm>, carrier <chr>, flight <int>, n <int>

Macht es die Abwesenheit von Duplikaten gleich zu einem guten Primärschlüssel? Ich denke nicht. Hier aber schon, da es irritierend für eine Fluglinie wäre, würden mehrere Flugzeuge mit derselben Flugnummer zur selben Zeit abheben.

Abgesehen davon, können wir einen Surrogat-Schlüssel durch die Zeilennummer konstruieren.

flights2 <- flights |> 
  mutate(id = row_number(), .before = 1)
flights2
## # A tibble: 336,776 × 20
##       id  year month   day dep_time sched_dep_time dep_delay arr_time
##    <int> <int> <int> <int>    <int>          <int>     <dbl>    <int>
##  1     1  2013     1     1      517            515         2      830
##  2     2  2013     1     1      533            529         4      850
##  3     3  2013     1     1      542            540         2      923
##  4     4  2013     1     1      544            545        -1     1004
##  5     5  2013     1     1      554            600        -6      812
##  6     6  2013     1     1      554            558        -4      740
##  7     7  2013     1     1      555            600        -5      913
##  8     8  2013     1     1      557            600        -3      709
##  9     9  2013     1     1      557            600        -3      838
## 10    10  2013     1     1      558            600        -2      753
## # ℹ 336,766 more rows
## # ℹ 12 more variables: sched_arr_time <int>, arr_delay <dbl>, carrier <chr>,
## #   flight <int>, tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>,
## #   distance <dbl>, hour <dbl>, minute <dbl>, time_hour <dttm>

Bei der Kommunikation zu einem anderen ist es einfach einfacher auf Flug 2001 zu verweisen, als auf Flug UA43, Abflugzeit 9 Uhr, am 21.3.2006.

Basic Joins

dplyr bietet sechs join Funktionen an: left, inner, right, full, semi, anti.

Mutating Joins

Erlaubt uns Variablen von zwei Data Frames zu kombinieren. Erst werden die Beobachtungen nach den Schlüsseln gematcht. Dann von einem Data Frame Variablen in den anderen kopiert. Die neuen Variablen werden rechts eingeordnet, so dass sie in der Console nicht unbedingt sichtbar sind. Wir bauen uns ein Dataset mit sechs Variablen.

flights2 <- flights |> 
  select(year, time_hour, origin, dest, tailnum, carrier)
flights2
## # A tibble: 336,776 × 6
##     year time_hour           origin dest  tailnum carrier
##    <int> <dttm>              <chr>  <chr> <chr>   <chr>  
##  1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA     
##  2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA     
##  3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA     
##  4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6     
##  5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL     
##  6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA     
##  7  2013 2013-01-01 06:00:00 EWR    FLL   N516JB  B6     
##  8  2013 2013-01-01 06:00:00 LGA    IAD   N829AS  EV     
##  9  2013 2013-01-01 06:00:00 JFK    MCO   N593JB  B6     
## 10  2013 2013-01-01 06:00:00 LGA    ORD   N3ALAA  AA     
## # ℹ 336,766 more rows

Meist benutzt man den left_join(). Der Output hat immer dieselbe Zeilenanzahl wie x. Der primäre Nutzen ist es Metadaten hinzuzufügen. So können wir den kompletten Airline Namen zu flights2 hinzufügen.

flights2 |>
  left_join(airlines)
## Joining with `by = join_by(carrier)`
## # A tibble: 336,776 × 7
##     year time_hour           origin dest  tailnum carrier name                  
##    <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>                 
##  1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      United Air Lines Inc. 
##  2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      United Air Lines Inc. 
##  3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      American Airlines Inc.
##  4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      JetBlue Airways       
##  5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      Delta Air Lines Inc.  
##  6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      United Air Lines Inc. 
##  7  2013 2013-01-01 06:00:00 EWR    FLL   N516JB  B6      JetBlue Airways       
##  8  2013 2013-01-01 06:00:00 LGA    IAD   N829AS  EV      ExpressJet Airlines I…
##  9  2013 2013-01-01 06:00:00 JFK    MCO   N593JB  B6      JetBlue Airways       
## 10  2013 2013-01-01 06:00:00 LGA    ORD   N3ALAA  AA      American Airlines Inc.
## # ℹ 336,766 more rows

Oder Wetterdaten zu den Abflugzeiten finden.

flights2 |> 
  left_join(weather |> select(origin, time_hour, temp, wind_speed))
## Joining with `by = join_by(time_hour, origin)`
## # A tibble: 336,776 × 8
##     year time_hour           origin dest  tailnum carrier  temp wind_speed
##    <int> <dttm>              <chr>  <chr> <chr>   <chr>   <dbl>      <dbl>
##  1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA       39.0       12.7
##  2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA       39.9       15.0
##  3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA       39.0       15.0
##  4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6       39.0       15.0
##  5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL       39.9       16.1
##  6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA       39.0       12.7
##  7  2013 2013-01-01 06:00:00 EWR    FLL   N516JB  B6       37.9       11.5
##  8  2013 2013-01-01 06:00:00 LGA    IAD   N829AS  EV       39.9       16.1
##  9  2013 2013-01-01 06:00:00 JFK    MCO   N593JB  B6       37.9       13.8
## 10  2013 2013-01-01 06:00:00 LGA    ORD   N3ALAA  AA       39.9       16.1
## # ℹ 336,766 more rows

Die Flugzeuggröße.

flights2 |> 
  left_join(planes |> select(tailnum, type, engines, seats))
## Joining with `by = join_by(tailnum)`
## # A tibble: 336,776 × 9
##     year time_hour           origin dest  tailnum carrier type     engines seats
##    <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>      <int> <int>
##  1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      Fixed w…       2   149
##  2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      Fixed w…       2   149
##  3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      Fixed w…       2   178
##  4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      Fixed w…       2   200
##  5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      Fixed w…       2   178
##  6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      Fixed w…       2   191
##  7  2013 2013-01-01 06:00:00 EWR    FLL   N516JB  B6      Fixed w…       2   200
##  8  2013 2013-01-01 06:00:00 LGA    IAD   N829AS  EV      Fixed w…       2    55
##  9  2013 2013-01-01 06:00:00 JFK    MCO   N593JB  B6      Fixed w…       2   200
## 10  2013 2013-01-01 06:00:00 LGA    ORD   N3ALAA  AA      <NA>          NA    NA
## # ℹ 336,766 more rows

Findet left_join() kein Match für eine Reihe in x, so wird mit NA aufgefüllt.

flights2 |> 
  filter(tailnum == "N3ALAA") |> 
  left_join(planes |> select(tailnum, type, engines, seats))
## Joining with `by = join_by(tailnum)`
## # A tibble: 63 × 9
##     year time_hour           origin dest  tailnum carrier type  engines seats
##    <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>   <int> <int>
##  1  2013 2013-01-01 06:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
##  2  2013 2013-01-02 18:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
##  3  2013 2013-01-03 06:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
##  4  2013 2013-01-07 19:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
##  5  2013 2013-01-08 17:00:00 JFK    ORD   N3ALAA  AA      <NA>       NA    NA
##  6  2013 2013-01-16 06:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
##  7  2013 2013-01-20 18:00:00 LGA    ORD   N3ALAA  AA      <NA>       NA    NA
##  8  2013 2013-01-22 17:00:00 JFK    ORD   N3ALAA  AA      <NA>       NA    NA
##  9  2013 2013-10-11 06:00:00 EWR    MIA   N3ALAA  AA      <NA>       NA    NA
## 10  2013 2013-10-14 08:00:00 JFK    BOS   N3ALAA  AA      <NA>       NA    NA
## # ℹ 53 more rows

Spezifizieren von Join-Schlüsseln

left_join() benutzt immer Variablen, die in beiden Data Frames auftauchen als Join Key: natural join. Manchmal funktioniert es nicht. So können Variablen mit gleichem Namen in verschiedenen Datensätzen eine andere Bedeutung haben.

flights2 |> 
  left_join(planes)
## Joining with `by = join_by(year, tailnum)`
## # A tibble: 336,776 × 13
##     year time_hour           origin dest  tailnum carrier type  manufacturer
##    <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr> <chr>       
##  1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      <NA>  <NA>        
##  2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      <NA>  <NA>        
##  3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      <NA>  <NA>        
##  4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      <NA>  <NA>        
##  5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      <NA>  <NA>        
##  6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      <NA>  <NA>        
##  7  2013 2013-01-01 06:00:00 EWR    FLL   N516JB  B6      <NA>  <NA>        
##  8  2013 2013-01-01 06:00:00 LGA    IAD   N829AS  EV      <NA>  <NA>        
##  9  2013 2013-01-01 06:00:00 JFK    MCO   N593JB  B6      <NA>  <NA>        
## 10  2013 2013-01-01 06:00:00 LGA    ORD   N3ALAA  AA      <NA>  <NA>        
## # ℹ 336,766 more rows
## # ℹ 5 more variables: model <chr>, engines <int>, seats <int>, speed <int>,
## #   engine <chr>

Hier hat year in beiden Datensätzen eine andere Bedeutung. flight$year ist das Jahr, in dem der Flug stattgefunden hat. planes$year ist das Jahr, in dem das Flugzeug gebaut wurde. Wir wollen aber nur auf tailnum joinen, also müssen wir eine explizite Spezifiation anbieten, mit join_by().

flights2 |> 
 # left_join(planes, join_by(tailnum)) muss noch aktualisiert werden
  left_join(planes, by = "tailnum")
## # A tibble: 336,776 × 14
##    year.x time_hour           origin dest  tailnum carrier year.y type          
##     <int> <dttm>              <chr>  <chr> <chr>   <chr>    <int> <chr>         
##  1   2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA        1999 Fixed wing mu…
##  2   2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA        1998 Fixed wing mu…
##  3   2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA        1990 Fixed wing mu…
##  4   2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6        2012 Fixed wing mu…
##  5   2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL        1991 Fixed wing mu…
##  6   2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA        2012 Fixed wing mu…
##  7   2013 2013-01-01 06:00:00 EWR    FLL   N516JB  B6        2000 Fixed wing mu…
##  8   2013 2013-01-01 06:00:00 LGA    IAD   N829AS  EV        1998 Fixed wing mu…
##  9   2013 2013-01-01 06:00:00 JFK    MCO   N593JB  B6        2004 Fixed wing mu…
## 10   2013 2013-01-01 06:00:00 LGA    ORD   N3ALAA  AA          NA <NA>          
## # ℹ 336,766 more rows
## # ℹ 6 more variables: manufacturer <chr>, model <chr>, engines <int>,
## #   seats <int>, speed <int>, engine <chr>

Die Variablen year wurden im gemeinsamen Datensatz jetzt optisch eindeutig gemacht, mit einem Zusatz (year.x, year.y), der genau sagt, wo die Variable herkommt: von x oder von y. Du kannst das Suffix natürlich überschreiben.
by = "tailnum" steht kurz für by = c("tailnum" = "tailnum") (evtl. join_by(tailnum) für join_by(tailnum == tailnum)).

Es gibt zwei Möglichkeiten die flights2 und die airports Tabelle zu verbinden: über dest oder über origin.

flights2 |> 
  left_join(airports, by = c("dest" = "faa"))
## # A tibble: 336,776 × 13
##     year time_hour           origin dest  tailnum carrier name         lat   lon
##    <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>      <dbl> <dbl>
##  1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      George Bu…  30.0 -95.3
##  2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      George Bu…  30.0 -95.3
##  3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      Miami Intl  25.8 -80.3
##  4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      <NA>        NA    NA  
##  5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      Hartsfiel…  33.6 -84.4
##  6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      Chicago O…  42.0 -87.9
##  7  2013 2013-01-01 06:00:00 EWR    FLL   N516JB  B6      Fort Laud…  26.1 -80.2
##  8  2013 2013-01-01 06:00:00 LGA    IAD   N829AS  EV      Washingto…  38.9 -77.5
##  9  2013 2013-01-01 06:00:00 JFK    MCO   N593JB  B6      Orlando I…  28.4 -81.3
## 10  2013 2013-01-01 06:00:00 LGA    ORD   N3ALAA  AA      Chicago O…  42.0 -87.9
## # ℹ 336,766 more rows
## # ℹ 4 more variables: alt <dbl>, tz <dbl>, dst <chr>, tzone <chr>
flights2 |> 
  left_join(airports, by = c("origin" = "faa"))
## # A tibble: 336,776 × 13
##     year time_hour           origin dest  tailnum carrier name         lat   lon
##    <int> <dttm>              <chr>  <chr> <chr>   <chr>   <chr>      <dbl> <dbl>
##  1  2013 2013-01-01 05:00:00 EWR    IAH   N14228  UA      Newark Li…  40.7 -74.2
##  2  2013 2013-01-01 05:00:00 LGA    IAH   N24211  UA      La Guardia  40.8 -73.9
##  3  2013 2013-01-01 05:00:00 JFK    MIA   N619AA  AA      John F Ke…  40.6 -73.8
##  4  2013 2013-01-01 05:00:00 JFK    BQN   N804JB  B6      John F Ke…  40.6 -73.8
##  5  2013 2013-01-01 06:00:00 LGA    ATL   N668DN  DL      La Guardia  40.8 -73.9
##  6  2013 2013-01-01 05:00:00 EWR    ORD   N39463  UA      Newark Li…  40.7 -74.2
##  7  2013 2013-01-01 06:00:00 EWR    FLL   N516JB  B6      Newark Li…  40.7 -74.2
##  8  2013 2013-01-01 06:00:00 LGA    IAD   N829AS  EV      La Guardia  40.8 -73.9
##  9  2013 2013-01-01 06:00:00 JFK    MCO   N593JB  B6      John F Ke…  40.6 -73.8
## 10  2013 2013-01-01 06:00:00 LGA    ORD   N3ALAA  AA      La Guardia  40.8 -73.9
## # ℹ 336,766 more rows
## # ℹ 4 more variables: alt <dbl>, tz <dbl>, dst <chr>, tzone <chr>

Joins filtern

Semi-Joins behalten alle Reihen in x, die ein Match in y haben. So können wir alle Flughäfen anzeigen, die passenden Origin haben:

airports |> 
  semi_join(flights2, by = c("faa" = "origin"))
## # A tibble: 3 × 8
##   faa   name                  lat   lon   alt    tz dst   tzone           
##   <chr> <chr>               <dbl> <dbl> <dbl> <dbl> <chr> <chr>           
## 1 EWR   Newark Liberty Intl  40.7 -74.2    18    -5 A     America/New_York
## 2 JFK   John F Kennedy Intl  40.6 -73.8    13    -5 A     America/New_York
## 3 LGA   La Guardia           40.8 -73.9    22    -5 A     America/New_York

Oder natürlich eine passenden Destination.

airports |> 
  semi_join(flights2, by = c("faa" = "dest"))
## # A tibble: 101 × 8
##    faa   name                                lat    lon   alt    tz dst   tzone 
##    <chr> <chr>                             <dbl>  <dbl> <dbl> <dbl> <chr> <chr> 
##  1 ABQ   Albuquerque International Sunport  35.0 -107.   5355    -7 A     Ameri…
##  2 ACK   Nantucket Mem                      41.3  -70.1    48    -5 A     Ameri…
##  3 ALB   Albany Intl                        42.7  -73.8   285    -5 A     Ameri…
##  4 ANC   Ted Stevens Anchorage Intl         61.2 -150.    152    -9 A     Ameri…
##  5 ATL   Hartsfield Jackson Atlanta Intl    33.6  -84.4  1026    -5 A     Ameri…
##  6 AUS   Austin Bergstrom Intl              30.2  -97.7   542    -6 A     Ameri…
##  7 AVL   Asheville Regional Airport         35.4  -82.5  2165    -5 A     Ameri…
##  8 BDL   Bradley Intl                       41.9  -72.7   173    -5 A     Ameri…
##  9 BGR   Bangor Intl                        44.8  -68.8   192    -5 A     Ameri…
## 10 BHM   Birmingham Intl                    33.6  -86.8   644    -6 A     Ameri…
## # ℹ 91 more rows

Anti-Joins sind das Gegenteil. Sie geben alle Reihen in x aus, die kein Match in y haben.

flights2 |> 
  anti_join(airports, by = c("dest" = "faa")) |> 
  distinct(dest)
## # A tibble: 4 × 1
##   dest 
##   <chr>
## 1 BQN  
## 2 SJU  
## 3 STT  
## 4 PSE

Welche tailnum fehlen in planes? Sie sind in flights2, aber nicht in planes.

flights2 |>
  anti_join(planes, by = "tailnum") |> 
  distinct(tailnum)
## # A tibble: 722 × 1
##    tailnum
##    <chr>  
##  1 N3ALAA 
##  2 N3DUAA 
##  3 N542MQ 
##  4 N730MQ 
##  5 N9EAMQ 
##  6 N532UA 
##  7 N3EMAA 
##  8 N518MQ 
##  9 N3BAAA 
## 10 N3CYAA 
## # ℹ 712 more rows

Wie funktionieren Joins?

x <- tribble(
  ~key, ~val_x,
     1, "x1",
     2, "x2",
     3, "x3"
)
y <- tribble(
  ~key, ~val_y,
     1, "y1",
     2, "y2",
     4, "y3"
)
x
## # A tibble: 3 × 2
##     key val_x
##   <dbl> <chr>
## 1     1 x1   
## 2     2 x2   
## 3     3 x3
y
## # A tibble: 3 × 2
##     key val_y
##   <dbl> <chr>
## 1     1 y1   
## 2     2 y2   
## 3     4 y3

Beim Inner Join matchen Reihen, wenn die Schlüssel gleich sind. Also enthält der Output nur Reihen mit Schlüsseln, die in x und y enthalten sind.

inner_join(x, y)
## Joining with `by = join_by(key)`
## # A tibble: 2 × 3
##     key val_x val_y
##   <dbl> <chr> <chr>
## 1     1 x1    y1   
## 2     2 x2    y2

Ein Outer Join behält Beobachtungen, die in mindestens einem der Data Frames auftauchen. Diese Beobachtung hat einen Schlüsel, der matcht, wenn es kein anderer Schlüssel tut. Ein NA wird dann erstellt. Drei Outer Joins existieren:

  • Left Join behält alle Beobachtungen in x. Jede Reihe von x wird beibehalten im Output.
left_join(x, y)
## Joining with `by = join_by(key)`
## # A tibble: 3 × 3
##     key val_x val_y
##   <dbl> <chr> <chr>
## 1     1 x1    y1   
## 2     2 x2    y2   
## 3     3 x3    <NA>
  • Right Join behält alle Beobachtungen in y.
right_join(x, y)
## Joining with `by = join_by(key)`
## # A tibble: 3 × 3
##     key val_x val_y
##   <dbl> <chr> <chr>
## 1     1 x1    y1   
## 2     2 x2    y2   
## 3     4 <NA>  y3
  • Full Join behält alle Beobachtungen, die in x oder y anfallen. Jede Reihe von x und y ist im Output vorhanden. Der Output fängt mit allen Reihen von x an, dann folgen die verbliebenden von y.
full_join(x, y)
## Joining with `by = join_by(key)`
## # A tibble: 4 × 3
##     key val_x val_y
##   <dbl> <chr> <chr>
## 1     1 x1    y1   
## 2     2 x2    y2   
## 3     3 x3    <NA> 
## 4     4 <NA>  y3

Row Matching

Was passiert, wenn eine Reihe in x zu mehr als einer Reihe in y matcht?

Es kann sein, dass eine Reihe in x:

  • nicht matcht. Sie entfällt.
  • zu einer Reihe matcht.
  • zu mehr als einer Reihe in y matcht. Sie wird dupliziert.

dplyr warnt uns, wann immer es multiple Matches gibt.

df1 <- tibble(key = c(1, 2, 3), val_x = c("x1", "x2", "x3"))
df2 <- tibble(key = c(1, 2, 4), val_y = c("y1", "y2", "y3"))
df1
## # A tibble: 3 × 2
##     key val_x
##   <dbl> <chr>
## 1     1 x1   
## 2     2 x2   
## 3     3 x3
df2
## # A tibble: 3 × 2
##     key val_y
##   <dbl> <chr>
## 1     1 y1   
## 2     2 y2   
## 3     4 y3
df1 |> 
  inner_join(df2, by = "key")
## # A tibble: 2 × 3
##     key val_x val_y
##   <dbl> <chr> <chr>
## 1     1 x1    y1   
## 2     2 x2    y2

Programmieren: Funktionen

Einleitung

In unserem eraten Abschnitt haben wir schon Funktionen eingeführt. Hier noch ein wenig intensiver. Funktionen erlauben es dir gewöhnliche Aufgaben zu automatisieren. Gegenüber copy-paste hat es drei Vorteile:

  1. Verpasse deiner Funktion einen Namen, so dass er einfacher zu verstehen ist.

  2. Verändern sich Voraussetzungen, so musst du Code nur an einem Ort aktualisieren, statt an vielen.

  3. Du verringerst die Chance gleiche Fehler wiederholt zu machen.

In diesem Abschnitt lernen wir drei Typen von Funktionen kennen:

  • Vektor-Funktionen, die einen oder mehrere Vektoren als Input nehmen und einen Vektor als Output ausgeben.

  • Data Frame Funktionen, die einen Data Frame als Input nehmen und einen Data Frame als Output ausgeben.

  • Plot-Funktionen, die einen Data Frame als Input nehmen und einen Plot als Output.

Voraussetzungen

library(tidyverse)
library(nycflights13)

Vektor-Funktionen

df <- tibble(
  a = rnorm(5),
  b = rnorm(5),
  c = rnorm(5),
  d = rnorm(5),
)
df
## # A tibble: 5 × 4
##         a      b      c       d
##     <dbl>  <dbl>  <dbl>   <dbl>
## 1  0.428   0.549  0.672 -1.14  
## 2  0.177  -1.55   1.16  -0.960 
## 3 -0.0642 -0.144  0.171 -1.33  
## 4 -1.69   -3.31   1.09   1.09  
## 5  0.964   2.27  -0.212 -0.0656
df |> mutate(
  a = (a - min(a, na.rm = TRUE)) / 
    (max(a, na.rm = TRUE) - min(a, na.rm = TRUE)),
  b = (b - min(b, na.rm = TRUE)) / 
    (max(b, na.rm = TRUE) - min(a, na.rm = TRUE)),
  c = (c - min(c, na.rm = TRUE)) / 
    (max(c, na.rm = TRUE) - min(c, na.rm = TRUE)),
  d = (d - min(d, na.rm = TRUE)) / 
    (max(d, na.rm = TRUE) - min(d, na.rm = TRUE)),
)
## # A tibble: 5 × 4
##       a     b     c      d
##   <dbl> <dbl> <dbl>  <dbl>
## 1 0.798 1.70  0.643 0.0773
## 2 0.703 0.775 1     0.153 
## 3 0.612 1.39  0.279 0     
## 4 0     0     0.948 1     
## 5 1     2.46  0     0.523

Alle Vektoren sollten zwischen 0 und 1 liegen. Tun sie aber nicht. Copy-Paste hat für b zu einem Fehler geführt. Also besser Funktionen schreiben.

Funktionen schreiben

Welche Teile des Codes sind jetzt konstant und welche variieren?

(a - min(a, na.rm = TRUE)) / (max(a, na.rm = TRUE) - min(a, na.rm = TRUE))
(b - min(b, na.rm = TRUE)) / (max(b, na.rm = TRUE) - min(b, na.rm = TRUE))
(c - min(c, na.rm = TRUE)) / (max(c, na.rm = TRUE) - min(c, na.rm = TRUE))
(d - min(d, na.rm = TRUE)) / (max(d, na.rm = TRUE) - min(d, na.rm = TRUE))  

In jeder Zeile sind es genau vier Buchstaben (jeweils: a, b, c, d).

(█ - min(█, na.rm = TRUE)) / (max(█, na.rm = TRUE) - min(█, na.rm = TRUE))

Um daraus eine Funktion zu machen, bedarf es drei Dinge:

  1. Name. Zum Beispiel rescale01, weil die Funktion einen Vektor in ein Intervall zwischen 0 und 1 packt.
  2. Argumente.
  3. Body. Der Code kommt hier herein.

Ein Template sieht dann wie folgt aus:

name <- function(arguments) {
  body
}

Er führt dann zu:

rescale01 <- function(x) {
  (x - min(x, na.rm = TRUE)) / (max(x, na.rm = TRUE) - min(x, na.rm = TRUE))
}
rescale01(c(-10, 0, 10))
## [1] 0.0 0.5 1.0
rescale01(c(1, 2, 3, NA, 5))
## [1] 0.00 0.25 0.50   NA 1.00

Mithilfe von mutate():

df |> mutate(
  a = rescale01(a),
  b = rescale01(b),
  c = rescale01(c),
  d = rescale01(d),
)
## # A tibble: 5 × 4
##       a     b     c      d
##   <dbl> <dbl> <dbl>  <dbl>
## 1 0.798 0.691 0.643 0.0773
## 2 0.703 0.315 1     0.153 
## 3 0.612 0.567 0.279 0     
## 4 0     0     0.948 1     
## 5 1     1     0     0.523

Funktion weiterentwickeln

Mithilfe von range() können wir schnell das Minimum und das Maximum berechnen.

rescale01 <- function(x) {
  rng <- range(x, na.rm = TRUE)
  (x - rng[1]) / (rng[2] - rng[1])
}

Liegt ein unendlicher Wert vor, so haben wir ein Problem.

x <- c(1:10, Inf)
rescale01(x)
##  [1]   0   0   0   0   0   0   0   0   0   0 NaN

Wir sagen also range() unendliche Werte bitte zu ignorieren.

rescale01 <- function(x) {
  rng <- range(x, na.rm = TRUE, finite = TRUE)
  (x - rng[1]) / (rng[2] - rng[1])
}

rescale01(x)
##  [1] 0.0000000 0.1111111 0.2222222 0.3333333 0.4444444 0.5555556 0.6666667
##  [8] 0.7777778 0.8888889 1.0000000       Inf

Mutate Funktionen

Wir wollen den z-Score berechnen. mutate() bietet sich hier an, da sie dieselbe Länge (wie der Input) ausgeben.

z_score <- function(x) {
  (x - mean(x, na.rm = TRUE)) / sd(x, na.rm = TRUE)
}
z_score(c(2,3,3,4))
## [1] -1.224745  0.000000  0.000000  1.224745

Mit case_when() können wir Werte ausgeben, die sich innerhalb eines Intervalls befinden.

# .default = x funktioniert nicht, auch nicht mit 1:10
clamp <- function(x, min, max) {
  case_when(
    x < min ~ min,
    x > max ~ max,
    .default = x
  )
}

clamp(1:10, min = 3, max = 7)
##  [1] 3 3 3 4 5 6 7 7 7 7
clamp <- function(x, min, max) {
  case_when(
    x < min ~ 3,
    x > max ~ 7,
    TRUE ~ x
  )
}

clamp(seq(1, 10, 1), min = 3, max = 7)
##  [1] 3 3 3 4 5 6 7 7 7 7

Willst du Dollarzeichen, Prozentzeichen, Komma von einem String entfernen?

clean_number <- function(x) {
    is_pct <- str_detect(x, "%")
    num <- x |> 
    str_remove_all("[%]") |> 
    str_remove_all(",") |> 
    str_remove_all("[$]") |> 
    as.numeric(x)
  if_else(is_pct, num / 100, num)
}

clean_number("$12,300")
## [1] 12300
clean_number("45%")
## [1] 0.45

Ersetze einen Vektor durch NA, wenn bestimmte Zahlen vorkommen.

fix_na <- function(x) {
  ifelse(x %in% c(997, 998, 999), NA, x)
}
fix_na(seq(990,1001,1))
##  [1]  990  991  992  993  994  995  996   NA   NA   NA 1000 1001

Unsere Funktion kann aber natürlich auch mehrere Vektoren als Argumente aufnehmen.

haversine <- function(long1, lat1, long2, lat2, round = 3) {
  # convert to radians
  long1 <- long1 * pi / 180
  lat1  <- lat1  * pi / 180
  long2 <- long2 * pi / 180
  lat2  <- lat2  * pi / 180
  
  R <- 6371 # Earth mean radius in km
  a <- sin((lat2 - lat1) / 2)^2 + 
    cos(lat1) * cos(lat2) * sin((long2 - long1) / 2)^2
  d <- R * 2 * asin(sqrt(a))
  
  round(d, round)
}

Zusammenfassende Funktionen (Summary Functions)

Summary Functions geben einen einzelnen Wert aus (summarize()).

commas <- function(x) {
  str_flatten(x, collapse = ", ", last = " and ")
}

commas(c("cat", "dog", "pigeon"))
## [1] "cat, dog and pigeon"

Berechne den Variationskoeffizienten:

cv <- function(x, na.rm = FALSE) {
  sd(x, na.rm = na.rm) / mean(x, na.rm = na.rm)
}

cv(runif(100, min = 0, max = 50))
## [1] 0.6416063
cv(runif(100, min = 0, max = 500))
## [1] 0.5875608

Wieviele Missing Values? Verpasse immer einen Namen, an den du dich erinnern kannst.

n_missing <- function(x) {
  sum(is.na(x))
} 
n_missing(c(2,3,4,NA,NA,5))
## [1] 2

Vergleiche zwei Vektoren miteinander.

mape <- function(actual, predicted) {
  sum(abs((actual - predicted) / actual)) / length(actual)
}
mape(c(1,2,3,2,3,4,2), c(2,3,2,3,4,4,2))
## [1] 0.3809524

Data Frame Funktionen

Vektorfunktionen sind nützlich, um Code herauszuziehen, der innerhalb eines dplyr-Verbs wiederholt wird. Aber oft wiederholen Sie auch die Verben selbst, insbesondere in einer großen Pipe. Wenn Sie feststellen, dass Sie mehrere Verben mehrmals kopieren und einfügen, sollten Sie über das Schreiben einer Data Frame Funktion nachdenken. Sie funktionieren ähnlich wie dplyr-Verben: Sie nehmen einen Data Frame als erstes Argument, einige zusätzliche Argumente, die angeben, was damit gemacht werden soll, und geben einen Data Frame oder Vektor zurück.

Indirection and Tidy Evaluation

Bei Verwendung von dolyr Verben und Funktionen, stößt man schnell auf Probleme.

grouped_mean <- function(df, group_var, mean_var) {
  df |> 
    group_by(group_var) |> 
    summarize(mean(mean_var))
}
diamonds |> grouped_mean(cut, carat)
#> Error in `group_by()`:
#> ! Must group by variables found in `.data`.
#> ✖ Column `group_var` is not found.

Machen wir es ein wenig deutlicher.

df <- tibble(
  mean_var = 1,
  group_var = "g",
  group = 1,
  x = 10,
  y = 100
)

df |> grouped_mean(group, x)
## # A tibble: 1 × 2
##   group_var `mean(mean_var)`
##   <chr>                <dbl>
## 1 g                        1
#> # A tibble: 1 × 2
#>   group_var `mean(mean_var)`
#>   <chr>                <dbl>
#> 1 g                        1
df |> grouped_mean(group, y)
## # A tibble: 1 × 2
##   group_var `mean(mean_var)`
##   <chr>                <dbl>
## 1 g                        1
#> # A tibble: 1 × 2
#>   group_var `mean(mean_var)`
#>   <chr>                <dbl>
#> 1 g                        1

Egal wie wir grouped_mean() nennen, es macht |> group_by(group_var) |> summarize(mean(mean_var)), statt df |> group_by(group) |> summarize(mean(x)). Embracing bedeutet, dass die Variable in geschweifte Klammern gepackt wird.

grouped_mean <- function(df, group_var, mean_var) {
  df |> 
    group_by({{ group_var }}) |> 
    summarize(mean({{ mean_var }}))
}

df |> grouped_mean(group, x)
## # A tibble: 1 × 2
##   group `mean(x)`
##   <dbl>     <dbl>
## 1     1        10

Wann Embrace?

Die Lösung findest du in der Dokumentation. * Data-masking: arrange(), filter(), summarize(). * Tidy-selection: select(), relocate(), rename().

Gewöhnliche Anwendungsfälle

summary6 <- function(data, var) {
  data |> summarize(
    min = min({{ var }}, na.rm = TRUE),
    mean = mean({{ var }}, na.rm = TRUE),
    median = median({{ var }}, na.rm = TRUE),
    max = max({{ var }}, na.rm = TRUE),
    n = n(),
    n_miss = sum(is.na({{ var }})),
    .groups = "drop"
  )
}

diamonds |> summary6(carat)
## # A tibble: 1 × 6
##     min  mean median   max     n n_miss
##   <dbl> <dbl>  <dbl> <dbl> <int>  <int>
## 1   0.2 0.798    0.7  5.01 53940      0

Das Schöne an dieser Funktion ist, dass wir sie auf gruppierten Daten verwenden können, da sie summarize() umschließt.

diamonds |> 
  group_by(cut) |> 
  summary6(carat)
## # A tibble: 5 × 7
##   cut         min  mean median   max     n n_miss
##   <ord>     <dbl> <dbl>  <dbl> <dbl> <int>  <int>
## 1 Fair       0.22 1.05    1     5.01  1610      0
## 2 Good       0.23 0.849   0.82  3.01  4906      0
## 3 Very Good  0.2  0.806   0.71  4    12082      0
## 4 Premium    0.2  0.892   0.86  4.01 13791      0
## 5 Ideal      0.2  0.703   0.54  3.5  21551      0

Berechnete Variablen können wir so auch mit summarize benutzen.

diamonds |> 
  group_by(cut) |> 
  summary6(log10(carat))
## # A tibble: 5 × 7
##   cut          min    mean  median   max     n n_miss
##   <ord>      <dbl>   <dbl>   <dbl> <dbl> <int>  <int>
## 1 Fair      -0.658 -0.0273  0      0.700  1610      0
## 2 Good      -0.638 -0.133  -0.0862 0.479  4906      0
## 3 Very Good -0.699 -0.164  -0.149  0.602 12082      0
## 4 Premium   -0.699 -0.125  -0.0655 0.603 13791      0
## 5 Ideal     -0.699 -0.225  -0.268  0.544 21551      0

Auch count() ist nützlich und berechnet Anteile.

count_prop <- function(df, var, sort = FALSE) {
  df |>
    count({{ var }}, sort = sort) |>
    mutate(prop = n / sum(n))
}

diamonds |> count_prop(clarity)
## # A tibble: 8 × 3
##   clarity     n   prop
##   <ord>   <int>  <dbl>
## 1 I1        741 0.0137
## 2 SI2      9194 0.170 
## 3 SI1     13065 0.242 
## 4 VS2     12258 0.227 
## 5 VS1      8171 0.151 
## 6 VVS2     5066 0.0939
## 7 VVS1     3655 0.0678
## 8 IF       1790 0.0332

Nur das zweite Argument der drei: df, var, sort muss umklammert werden, da count() data-masking für alle Variablen benutzt. sort hat als default (Wert) FALSE.

unique_where <- function(df, condition, var) {
  df |> 
    filter({{ condition }}) |> 
    distinct({{ var }}) |> 
    arrange({{ var }})
}

# Find all the destinations in December
flights |> unique_where(month == 12, dest)
## # A tibble: 96 × 1
##    dest 
##    <chr>
##  1 ABQ  
##  2 ALB  
##  3 ATL  
##  4 AUS  
##  5 AVL  
##  6 BDL  
##  7 BGR  
##  8 BHM  
##  9 BNA  
## 10 BOS  
## # ℹ 86 more rows

Wenn du immer mit demselben Datensatz arbeitest, kann es Sinn machen den Datensatz fest einzuprogrammieren. Als Spalte kann eine Zahl entsprechend der Spaltenzahl dienen, oder der Name mit, oder ohne Anführungszeichen.

subset_flights <- function(rows, cols) {
  flights |> 
    filter({{ rows }}) |> 
    select(time_hour, carrier, flight, {{ cols }})
}
subset_flights(TRUE, "year")
## # A tibble: 336,776 × 4
##    time_hour           carrier flight  year
##    <dttm>              <chr>    <int> <int>
##  1 2013-01-01 05:00:00 UA        1545  2013
##  2 2013-01-01 05:00:00 UA        1714  2013
##  3 2013-01-01 05:00:00 AA        1141  2013
##  4 2013-01-01 05:00:00 B6         725  2013
##  5 2013-01-01 06:00:00 DL         461  2013
##  6 2013-01-01 05:00:00 UA        1696  2013
##  7 2013-01-01 06:00:00 B6         507  2013
##  8 2013-01-01 06:00:00 EV        5708  2013
##  9 2013-01-01 06:00:00 B6          79  2013
## 10 2013-01-01 06:00:00 AA         301  2013
## # ℹ 336,766 more rows
subset_flights(nrow(flights) <= 10, 1)
## # A tibble: 0 × 4
## # ℹ 4 variables: time_hour <dttm>, carrier <chr>, flight <int>, year <int>

Data-masking vs. Tidy-selection

Manchmal möchtest du Variablen innerhalb einer Funktion auswählen, die Datenmaskierung verwendet

count_missing <- function(df, group_vars, x_var) {
  df |> 
    group_by({{ group_vars }}) |> 
    summarize(
      n_miss = sum(is.na({{ x_var }})),
      .groups = "drop"
    )
}

flights |> 
  count_missing(c(year, month, day), dep_time)
#> Error in `group_by()`:
#> ℹ In argument: `c(year, month, day)`.
#> Caused by error:
#> ! `c(year, month, day)` must be size 336776 or 1, not 1010328.

Tidy-selection in einer data-masking Funktion lässt sich benutzen mit pick(). Der Code oben funktioniert nicht, da group_by() data-masking, nicht tidy-selection benutzt.

count_missing <- function(df, group_vars, x_var) {
  df |> 
    group_by(pick({{ group_vars }})) |> 
    summarize(
      n_miss = sum(is.na({{ x_var }})),
      .groups = "drop"
  )
}

flights |> 
  count_missing(c(year, month, day), dep_time)
## # A tibble: 365 × 4
##     year month   day n_miss
##    <int> <int> <int>  <int>
##  1  2013     1     1      4
##  2  2013     1     2      8
##  3  2013     1     3     10
##  4  2013     1     4      6
##  5  2013     1     5      3
##  6  2013     1     6      1
##  7  2013     1     7      3
##  8  2013     1     8      4
##  9  2013     1     9      5
## 10  2013     1    10      3
## # ℹ 355 more rows
count_wide <- function(data, rows, cols) {
  data |> 
    count(pick(c({{ rows }}, {{ cols }}))) |> 
    pivot_wider(
      names_from = {{ cols }}, 
      values_from = n,
      names_sort = TRUE,
      values_fill = 0
    )
}

diamonds |> count_wide(c(clarity, color), cut)
## # A tibble: 56 × 7
##    clarity color  Fair  Good `Very Good` Premium Ideal
##    <ord>   <ord> <int> <int>       <int>   <int> <int>
##  1 I1      D         4     8           5      12    13
##  2 I1      E         9    23          22      30    18
##  3 I1      F        35    19          13      34    42
##  4 I1      G        53    19          16      46    16
##  5 I1      H        52    14          12      46    38
##  6 I1      I        34     9           8      24    17
##  7 I1      J        23     4           8      13     2
##  8 SI2     D        56   223         314     421   356
##  9 SI2     E        78   202         445     519   469
## 10 SI2     F        89   201         343     523   453
## # ℹ 46 more rows

Plot Funktionen

Anstatt eines Data Frames wollen wir einen Plot ausgeben lassen. Du kannst dieselbe Technik mit ggplot2 verwenden, da aes() eine data-masking Funktion ist.

diamonds |> 
  ggplot(aes(x = carat)) +
  geom_histogram(binwidth = 0.1)

diamonds |> 
  ggplot(aes(x = carat)) +
  geom_histogram(binwidth = 0.05)

Es wäre aber doch viel schöner, wenn du diesen Code in eine Histogramm Funktion packen könntest.

histogram <- function(df, var, binwidth = NULL) {
  df |> 
    ggplot(aes(x = {{ var }})) + 
    geom_histogram(binwidth = binwidth)
}

diamonds |> histogram(carat, 0.1)

Du kannst natürlich noch durch + weitere Komponenten hinzufügen.

diamonds |> 
  histogram(carat, 0.1) +
  labs(x = "Size (in carats)", y = "Number of diamonds")

Mehr Variablen

Mehr Variablen können natürlich hinzugefügt werden.

linearity_check <- function(df, x, y) {
  df |>
    ggplot(aes(x = {{ x }}, y = {{ y }})) +
    geom_point() +
    geom_smooth(method = "loess", formula = y ~ x, color = "red", se = FALSE) +
    geom_smooth(method = "lm", formula = y ~ x, color = "blue", se = FALSE) 
}

starwars |> 
  filter(mass < 1000) |> 
  linearity_check(mass, height)

hex_plot <- function(df, x, y, z, bins = 20, fun = "mean") {
  df |> 
    ggplot(aes(x = {{ x }}, y = {{ y }}, z = {{ z }})) + 
    stat_summary_hex(
      aes(color = after_scale(fill)), # make border same color as fill
      bins = bins, 
      fun = fun,
    )
}

diamonds |> hex_plot(carat, price, depth)

Kombinieren mit tidyverse

Wir wollen ein vertikales Säulendiagramm erstellen, bei dem die Reihenfolge fallend, statt aufsteigend ist.

sorted_bars <- function(df, var) {
  df |> 
    mutate({{ var }} := fct_rev(fct_infreq({{ var }})))  |>
    ggplot(aes(y = {{ var }})) +
    geom_bar()
}

diamonds |> sorted_bars(clarity)

Hier haben wir eine neuen Operator, :=. R erlaubt hier nur einen einfachen Wortnamen, wir wollen aber unsere Variable überschreiben. Von tidy wird er wie ein = bewertet.

Einen Plot für eine Teilmenge mithilfe von filter() in einer Funktion können wir auch leicht erstellen.

conditional_bars <- function(df, condition, var) {
  df |> 
    filter({{ condition }}) |> 
    ggplot(aes(x = {{ var }})) + 
    geom_bar()
}

diamonds |> conditional_bars(cut == "Good", clarity)

Labeling

histogram <- function(df, var, binwidth = NULL) {
  df |> 
    ggplot(aes(x = {{ var }})) + 
    geom_histogram(binwidth = binwidth)
}

Warum nicht hier eine Überschrift hinzufügen? Dazu benutzen wir das rlang Paket. Dazu benutzen wir rlang::englue(). Jeder Wert in {} wird in den String eingeführt. In {{}} wird der Variablenname eingesetzt.

histogram <- function(df, var, binwidth) {
  label <- rlang::englue("A histogram of {{var}} with binwidth {binwidth}")
  
  df |> 
    ggplot(aes(x = {{ var }})) + 
    geom_histogram(binwidth = binwidth) + 
    labs(title = label)
}

diamonds |> histogram(carat, 0.1)

Style

R ist es egal wie du deine Funktionen oder Namen benennst. Kurz sollten sie sein, aber auch sollte man eine Idee bekommen wie sich die Funktion verhält. Funktionsnamen sind meist verben und Argumente Nomen.

# Too short
f()

# Not a verb, or descriptive
my_awesome_function()

# Long, but clear
impute_missing()
collapse_years()

Einrücken der Zeilen nicht vergessen.

# Missing extra two spaces
density <- function(color, facets, binwidth = 0.1) {
diamonds |> 
  ggplot(aes(x = carat, y = after_stat(density), color = {{ color }})) +
  geom_freqpoly(binwidth = binwidth) +
  facet_wrap(vars({{ facets }}))
}

# Pipe indented incorrectly
density <- function(color, facets, binwidth = 0.1) {
  diamonds |> 
  ggplot(aes(x = carat, y = after_stat(density), color = {{ color }})) +
  geom_freqpoly(binwidth = binwidth) +
  facet_wrap(vars({{ facets }}))
}

Iteration

Einleitung und Voraussetzung

Willst du einen numerischen Vektor verdoppeln, so reicht es einfach 2 * x zu schreiben. Ähnliche Werkzeuge für Wiederholungen haben wir schon kennengelernt:

  • facet_wrap() und facet_grid zeichnet einen Plot für jede Teilmenge.
  • group_by mit summarize() berechnet zusammenfassende Statistiken für Untergruppen.
  • unnest_wider() und unnest_longer() erstellen neue Zeilen und Spalten.

Jetzt lernen wir generelle Werkzeuge, functional programming Werkzeuge kennen. Sie werden so genannt, da sie um Funktionen gebaut werden, die andere Funktionen als Input nehmen.

library(tidyverse)

Wir brauchen das bekannte dplyr und das neue purrr. Ein hervorrangendes Paket.

Modyfying Multiple Columns

Wir schauen uns ein simplen tibble an und wollen Anzahl der Beobachtungen, sowie Median, jeder Spalte berechen.

df <- tibble(
  a = rnorm(10),
  b = rnorm(10),
  c = rnorm(10),
  d = rnorm(10)
)
df
## # A tibble: 10 × 4
##          a       b       c      d
##      <dbl>   <dbl>   <dbl>  <dbl>
##  1 -2.59    0.335   0.541  -0.424
##  2  0.201  -0.337  -0.523   0.122
##  3 -0.0302 -0.289  -1.39    0.540
##  4  0.585  -0.0502  0.370   1.23 
##  5 -0.499  -0.696  -0.0552 -0.455
##  6  1.30    0.261  -1.26    1.62 
##  7 -1.76    0.344  -0.317  -0.641
##  8  2.29   -2.62    2.21   -2.93 
##  9  1.26    0.657   0.724   0.201
## 10  0.580  -0.427   0.620  -0.864
df |> summarize(
  n = n(),
  a = median(a),
  b = median(b),
  c = median(c),
  d = median(d),
)
## # A tibble: 1 × 5
##       n     a      b     c      d
##   <int> <dbl>  <dbl> <dbl>  <dbl>
## 1    10 0.390 -0.170 0.158 -0.151

Wir können es mit copy-paste erledigen. Das kann sehr mühsam sein, Oder wir benutzen across():

df |> summarize(
  n = n(),
  across(a:d, median),
)
## # A tibble: 1 × 5
##       n     a      b     c      d
##   <int> <dbl>  <dbl> <dbl>  <dbl>
## 1    10 0.390 -0.170 0.158 -0.151

Es hat drei wichtige Argumente, wobei die ersten beiden elementar sind: .cols bestimmt die Spalten über die iteriert werden soll und .fns was mit jeder Spalte gemacht werden soll. Das .names Argument benutzt du, wenn du zusätzlich Kontrolle über über die Namen des Outputs gewinnen willst.

Auswahl von Spalten mit .cols

Das erste Argument sucht die zu transformierenden Spalten aus. Es benutzt dieselbe Spezifikation wie select(), sodass du auch starts_with() und ends_with() benutzen kannst.

dfs <- tibble( w1 = rnorm(4), w2 = rnorm(4) + 5, s2 = rnorm(4) * 100)
dfs
## # A tibble: 4 × 3
##       w1    w2     s2
##    <dbl> <dbl>  <dbl>
## 1  0.781  7.39  -60.2
## 2 -0.887  5.69 -210. 
## 3  1.57   6.17   83.7
## 4  1.65   3.32  201.
dfs|>
  select(starts_with("w"))
## # A tibble: 4 × 2
##       w1    w2
##    <dbl> <dbl>
## 1  0.781  7.39
## 2 -0.887  5.69
## 3  1.57   6.17
## 4  1.65   3.32
dfs|>
  summarize(
    n = n(),
    across(c(w1, w2, s2), median)
  )
## # A tibble: 1 × 4
##       n    w1    w2    s2
##   <int> <dbl> <dbl> <dbl>
## 1     4  1.18  5.93  11.7

Zwei weitere Auswahltechniken sind sehr nützlich für across(): everything und where(). everything ist straightforward: es wählt jede nicht-gruppierte Spalte.

df <- tibble(
  grp = sample(2, 10, replace = TRUE),
  a = rnorm(10),
  b = rnorm(10),
  c = rnorm(10),
  d = rnorm(10)
)

df |> 
  group_by(grp) |> 
  summarize(across(everything(), median))
## # A tibble: 2 × 5
##     grp      a     b        c       d
##   <int>  <dbl> <dbl>    <dbl>   <dbl>
## 1     1 -0.241 0.194 -0.253   -0.0615
## 2     2  0.760 0.919  0.00556  0.197

where() erlaubt es Spalten aufgrund ihres Types auszuwählen: - where(is.numeric) - where(is.Date) - …

Du kannst die Selektoren miteinander kombinieren: starts_with("a") & where(is.logical). Hier werden alle logischen Vektoren ausgewählt, deren Namen mit “a” starten.

Eine einfach Funktion aufrufen

Wir übergeben eine Funktion an eine andere. Wir übergeben diese Funktion an across(). Wir rufen sie nicht selbst auf. Dem Funktionsnamen folgt kein (). Sonst gibt es eine Fehlermeldung. Besser also ohne.

k =  function(x) mean(x)
df|>
  group_by(grp)|>
  summarize(across(everything(), k))
## # A tibble: 2 × 5
##     grp      a     b       c      d
##   <int>  <dbl> <dbl>   <dbl>  <dbl>
## 1     1 -0.552 0.168 -0.608  -0.221
## 2     2  0.692 0.531 -0.0817  0.135

Multiple Funktionen aufrufen

Haben wir Missing Values in unserem Datensatz, so wollen wir diese natürlich entfernen.

rnorm_na <- function(n, n_na, mean = 0, sd = 1) {
  sample(c(rnorm(n - n_na, mean = mean, sd = sd), rep(NA, n_na)))
}

df_miss <- tibble(
  a = rnorm_na(5, 1),
  b = rnorm_na(5, 1),
  c = rnorm_na(5, 2),
  d = rnorm(5)
)
df_miss |> 
  summarize(
    across(a:d, median),
    n = n()
  )
## # A tibble: 1 × 5
##       a     b     c      d     n
##   <dbl> <dbl> <dbl>  <dbl> <int>
## 1    NA    NA    NA 0.0920     5

Das können wir natürlich leicht mithilfe von na.rm = T. Statt den Median durch median() aufzurufen, müssen wir eine neue Funktion kreieren.

df_miss |> 
  summarize(
    across(a:d, function(x) median(x, na.rm = TRUE)),
    n = n()
  )
## # A tibble: 1 × 5
##       a       b     c      d     n
##   <dbl>   <dbl> <dbl>  <dbl> <int>
## 1 0.328 -0.0381 0.136 0.0920     5

Es geht aber noch ein wenig kürzer, indem man function durch \ ersetzt.

df_miss |> 
  summarize(
    across(a:d, \(x) median(x, na.rm = TRUE)),
    n = n()
  )
## # A tibble: 1 × 5
##       a       b     c      d     n
##   <dbl>   <dbl> <dbl>  <dbl> <int>
## 1 0.328 -0.0381 0.136 0.0920     5

Was geht wohl schneller?

df_miss |> 
  summarize(
    a = median(a, na.rm = TRUE),
    b = median(b, na.rm = TRUE),
    c = median(c, na.rm = TRUE),
    d = median(d, na.rm = TRUE),
    n = n()
  )
## # A tibble: 1 × 5
##       a       b     c      d     n
##   <dbl>   <dbl> <dbl>  <dbl> <int>
## 1 0.328 -0.0381 0.136 0.0920     5

Wir können jetzt sogar noch eine weitere Funktion hinzufügen. In eine Liste.

df_miss |> 
  summarize(
    across(a:d, list(
      median = \(x) median(x, na.rm = TRUE),
      n_miss = \(x) sum(is.na(x))
    )),
    n = n()
  )
## # A tibble: 1 × 9
##   a_median a_n_miss b_median b_n_miss c_median c_n_miss d_median d_n_miss     n
##      <dbl>    <int>    <dbl>    <int>    <dbl>    <int>    <dbl>    <int> <int>
## 1    0.328        1  -0.0381        1    0.136        2   0.0920        0     5

Achte auf die neuen Spaltennamen. Das ist kein Zufall: {.col}_{.fn}. Der Name ist eine Kombination aus Spaltenname und Funktion.

Spaltennamen

Die können wir jetzt selber festlegen, wenn wir z.B. zuerst den Namen der Funktion uns wünschen.

df_miss |> 
  summarize(
    across(
      a:d,
      list(
        median = \(x) median(x, na.rm = TRUE),
        n_miss = \(x) sum(is.na(x))
      ),
      .names = "{.fn}_{.col}"
    ),
    n = n(),
  )
## # A tibble: 1 × 9
##   median_a n_miss_a median_b n_miss_b median_c n_miss_c median_d n_miss_d     n
##      <dbl>    <int>    <dbl>    <int>    <dbl>    <int>    <dbl>    <int> <int>
## 1    0.328        1  -0.0381        1    0.136        2   0.0920        0     5

Das .names Argument ist besonders wichtig, wenn du across() zusammen mit mutate() benutzt. across() innerhalb von mutate() ersetzt existierende Spalten. In unserem Fall ersetzt coalesce() NA durch 0.

df_miss |> 
  mutate(
    across(a:d, \(x) coalesce(x, 0))
  )
## # A tibble: 5 × 4
##        a       b      c       d
##    <dbl>   <dbl>  <dbl>   <dbl>
## 1  0.323  0       0      0.550 
## 2 -0.525 -0.259   0.136  0.0920
## 3  0.334  0.291  -0.736 -0.439 
## 4  0     -0.146   0      0.298 
## 5  0.440  0.0696  0.660 -1.28

Du kannst aber auch neue Spalten kreieren, indem du durch .names dem Output neue Namen verpasst.

df_miss |> 
  mutate(
    across(a:d, \(x) abs(x), .names = "{.col}_abs")
  )
## # A tibble: 5 × 8
##        a       b      c       d  a_abs   b_abs  c_abs  d_abs
##    <dbl>   <dbl>  <dbl>   <dbl>  <dbl>   <dbl>  <dbl>  <dbl>
## 1  0.323 NA      NA      0.550   0.323 NA      NA     0.550 
## 2 -0.525 -0.259   0.136  0.0920  0.525  0.259   0.136 0.0920
## 3  0.334  0.291  -0.736 -0.439   0.334  0.291   0.736 0.439 
## 4 NA     -0.146  NA      0.298  NA      0.146  NA     0.298 
## 5  0.440  0.0696  0.660 -1.28    0.440  0.0696  0.660 1.28

Filtern

across() funktioniert sehr gut mit summarize() und mutate(), aber nicht wirklich mit filter(). dplyr bietet zwei Varianten von across()an: if_any() und if_all().

# same as df_miss |> filter(is.na(a) | is.na(b) | is.na(c) | is.na(d))
df_miss |> filter(if_any(a:d, is.na))
## # A tibble: 2 × 4
##        a      b     c     d
##    <dbl>  <dbl> <dbl> <dbl>
## 1  0.323 NA        NA 0.550
## 2 NA     -0.146    NA 0.298

Jede Zeile wird übernommen, in der mindestens ein NA Wert ist. Oder in der nur NA Werte sind.

# same as df_miss |> filter(is.na(a) & is.na(b) & is.na(c) & is.na(d))
df_miss |> filter(if_all(a:d, is.na))
## # A tibble: 0 × 4
## # ℹ 4 variables: a <dbl>, b <dbl>, c <dbl>, d <dbl>

across() in Funktionen

across() ist sehr nützlich, da es einem erlaubt auch auf multiplen Spalten zu operieren.

expand_dates <- function(df) {
  df |> 
    mutate(
      across(where(is.Date), list(year = year, month = month, day = mday))
    )
}

df_date <- tibble(
  name = c("Amy", "Bob"),
  date = ymd(c("2009-08-03", "2010-01-16"))
)

df_date |> 
  expand_dates()
## # A tibble: 2 × 5
##   name  date       date_year date_month date_day
##   <chr> <date>         <dbl>      <dbl>    <int>
## 1 Amy   2009-08-03      2009          8        3
## 2 Bob   2010-01-16      2010          1       16

Berechne das arithmetische Mittel von numerischen Spalten.

summarize_means <- function(df, summary_vars = where(is.numeric)) {
  df |> 
    summarize(
      across({{ summary_vars }}, \(x) mean(x, na.rm = TRUE)),
      n = n()
    )
}
diamonds |> 
  group_by(cut) |> 
  summarize_means()
## # A tibble: 5 × 11
##   cut       carat depth table price     x     y     z log_price log_carat     n
##   <ord>     <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>     <dbl>     <dbl> <int>
## 1 Fair      1.05   64.0  59.1 4359.  6.25  6.18  3.98      8.09   -0.0629  1610
## 2 Good      0.849  62.4  58.7 3929.  5.84  5.85  3.64      7.84   -0.307   4906
## 3 Very Good 0.806  61.8  58.0 3982.  5.74  5.77  3.56      7.80   -0.379  12082
## 4 Premium   0.892  61.3  58.7 4584.  5.97  5.94  3.65      7.95   -0.288  13791
## 5 Ideal     0.703  61.7  56.0 3458.  5.51  5.52  3.40      7.64   -0.518  21551
diamonds |> 
  group_by(cut) |> 
  summarize_means(c(carat, x:z))
## # A tibble: 5 × 6
##   cut       carat     x     y     z     n
##   <ord>     <dbl> <dbl> <dbl> <dbl> <int>
## 1 Fair      1.05   6.25  6.18  3.98  1610
## 2 Good      0.849  5.84  5.85  3.64  4906
## 3 Very Good 0.806  5.74  5.77  3.56 12082
## 4 Premium   0.892  5.97  5.94  3.65 13791
## 5 Ideal     0.703  5.51  5.52  3.40 21551

vs. pivot_longer()

Mithilfe von pivot_longer konnten wir aus einer Kreuztabelle, eine lange Tabelle machen. Oftmals wird erst pivot_longer() auf die Tabelle angesetzt, dann folgt group_by().

df |> 
  summarize(across(a:d, list(median = median, mean = mean)))
## # A tibble: 1 × 8
##   a_median  a_mean b_median b_mean c_median c_mean d_median  d_mean
##      <dbl>   <dbl>    <dbl>  <dbl>    <dbl>  <dbl>    <dbl>   <dbl>
## 1    0.188 -0.0543    0.610  0.313   -0.106 -0.397  0.00790 -0.0785

Dieselben Werte erhalten wir natürlich, indem wir pivot_longer() benutzen und dann summarize().

long <- df |> 
  pivot_longer(a:d) |> 
  group_by(name) |> 
  summarize(
    median = median(value),
    mean = mean(value)
  )

Benutze pivot_wider() um wieder zur alten Struktur zurückzukehren.

long |> 
  pivot_wider(
    names_from = name,
    values_from = c(median, mean),
    names_vary = "slowest",
    names_glue = "{name}_{.value}"
  )
## # A tibble: 1 × 8
##   a_median  a_mean b_median b_mean c_median c_mean d_median  d_mean
##      <dbl>   <dbl>    <dbl>  <dbl>    <dbl>  <dbl>    <dbl>   <dbl>
## 1    0.188 -0.0543    0.610  0.313   -0.106 -0.397  0.00790 -0.0785

Es ist eine nützliche Technik, auch wenn die Namen der Variablen “schwierig” sind.

df_paired <- tibble(
  a_val = rnorm(10),
  a_wts = runif(10),
  b_val = rnorm(10),
  b_wts = runif(10),
  c_val = rnorm(10),
  c_wts = runif(10),
  d_val = rnorm(10),
  d_wts = runif(10)
)

Keine Chance mit across(), aber mit pivot_longer().

df_long <- df_paired |> 
  pivot_longer(
    everything(), 
    names_to = c("group", ".value"), 
    names_sep = "_"
  )
df_long
## # A tibble: 40 × 3
##    group    val    wts
##    <chr>  <dbl>  <dbl>
##  1 a     -0.428 0.678 
##  2 b     -0.512 0.661 
##  3 c      1.21  0.406 
##  4 d     -0.260 0.205 
##  5 a     -0.188 0.628 
##  6 b     -0.760 0.0532
##  7 c      0.156 0.0774
##  8 d      0.396 0.973 
##  9 a      1.86  0.673 
## 10 b      1.88  0.0996
## # ℹ 30 more rows
df_long |> 
  group_by(group) |> 
  summarize(mean = weighted.mean(val, wts))
## # A tibble: 4 × 2
##   group  mean
##   <chr> <dbl>
## 1 a     0.221
## 2 b     0.325
## 3 c     0.190
## 4 d     0.332

Reading Multiple Files

Hier geht es darum purrr::map() zu benutzen, dass Transformationen nicht in multiplen Spalten vornimmt, sondern in jedem Ordner deiner directory. Da das Thema sehr speziell ist, schieben wir es mal nach hinten. Genauso wie das nächste.

Saving Multiple Outputs

Speziell, da geschoben.

Base R - R Funktionen

Einleitung

Wir haben viel tidyverse benutzt, aber es geht auch ohne Pakete. Trotzdem laden wir unser Standardpaket.

library(tidyverse)

Auswahl multipler Elemente mit [

Eckige Klammern werden benutzt, um Teilmengen aus Vektoren oder Data Frames zu gewinnen. Viele dplyr Verben sind Spizialfälle von [.

Subsetting Vectors

Es gibt 5 Möglichkeiten einen Vektor zu unterteilen. x[i] wäre ein Beispiel:

  1. Ein Vektor mit positiven, ganzen Zahlen. Ein Vektor, oder eine einfache Zahl werden gewählt. Sie bestimmen die Position des Elements im Vektor, das ausgegeben wird. Ein Beispiel zeigt es schnell. Mehrere Zahlen können wiederholt werden, so dass der Output länger ist als der Input. In tidyverse können wir mit slice(1:2) z.B. Zeilen auswählen.
x <- c("one", "two", "three", "four", "five")
x[c(3, 2, 5)]
## [1] "three" "two"   "five"
x[2]
## [1] "two"
x[c(1, 1, 5, 5, 5, 2)]
## [1] "one"  "one"  "five" "five" "five" "two"
  1. Ein Vektor mit negativen Zahlen. Sie entfernen die Zahlen hinter dem -. Bitte nicht + und - mischen:
x[c(-1, -3, -5)]
## [1] "two"  "four"
  1. Ein logical vector. Sehr wichtig. Der Vektor behält die Elemente, die mit einem TRUE korrespondieren. Bei Vergleichen wird es oft gebraucht.
x <- c(10, 3, NA, 5, 8, 1, NA)

# All non-missing values of x
x[!is.na(x)]
## [1] 10  3  5  8  1
# All even (or missing!) values of x
x[x %% 2 == 0]
## [1] 10 NA  8 NA
  1. Ein Character Vector. Hast du einen benannten Vektor, so kannst du ihn mit seinem Namen ansprechen.
x <- c(abc = 1, def = 2, xyz = 5)
x[c("xyz", "def")]
## xyz def 
##   5   2
  1. Nothing. Klingt unlogisch, spielt aber später bei 2d Strukturen wie tibbles eine Rolle.

Subsetting Data Frames

df[rows, cols] wählt den Wert eines Data Frames aus. Lasse ich eine Seite weg, so werden ALLE Werte der Zeile oder Spalte weggelassen.

df <- tibble(
  x = 1:3, 
  y = c("a", "e", "f"), 
  z = runif(3)
)
df
## # A tibble: 3 × 3
##       x y         z
##   <int> <chr> <dbl>
## 1     1 a     0.104
## 2     2 e     0.402
## 3     3 f     0.770
# Select first row and second column
df[1, 2]
## # A tibble: 1 × 1
##   y    
##   <chr>
## 1 a
# Select all rows and columns x and y
df[, c("x" , "y")]
## # A tibble: 3 × 2
##       x y    
##   <int> <chr>
## 1     1 a    
## 2     2 e    
## 3     3 f
# Select rows where `x` is greater than 1 and all columns
df[df$x > 1, ]
## # A tibble: 2 × 3
##       x y         z
##   <int> <chr> <dbl>
## 1     2 e     0.402
## 2     3 f     0.770

$ bei df$x wählt die Variable x von df aus. Es gibt einen Unterschied zwischen tibbles und Data Frames bzgl [. Bei einem Data Frame wird ein Vektor erzeugt, wenn nur eine Variable ausgewählt wird. Bei einem tibble wird immer wieder ein tibble erzeugt.

df1 <- data.frame(x = 1:3)
df1[, "x"]
## [1] 1 2 3
df2 <- tibble(x = 1:3)
df2[, "x"]
## # A tibble: 3 × 1
##       x
##   <int>
## 1     1
## 2     2
## 3     3

Verhindere es mit:

df1[, "x" , drop = FALSE]
##   x
## 1 1
## 2 2
## 3 3

Äquivalente zu dplyr

  1. filter() ist äquivalent zu Subsetting die Reihen mithilfe eines logischen Vektors.
df <- tibble(
  x = c(2, 3, 1, 1, NA), 
  y = letters[1:5], 
  z = runif(5)
)
df |> filter(x > 1)
## # A tibble: 2 × 3
##       x y         z
##   <dbl> <chr> <dbl>
## 1     2 a     0.347
## 2     3 b     0.523
# same as
df[!is.na(df$x) & df$x > 1, ]
## # A tibble: 2 × 3
##       x y         z
##   <dbl> <chr> <dbl>
## 1     2 a     0.347
## 2     3 b     0.523
which(df$x > 1)
## [1] 1 2
df[which(df$x > 1), ]
## # A tibble: 2 × 3
##       x y         z
##   <dbl> <chr> <dbl>
## 1     2 a     0.347
## 2     3 b     0.523
  1. arrange() zu order():
df |> arrange(x, y)
## # A tibble: 5 × 3
##       x y         z
##   <dbl> <chr> <dbl>
## 1     1 c     0.368
## 2     1 d     0.851
## 3     2 a     0.347
## 4     3 b     0.523
## 5    NA e     0.934
df |> arrange(x, y, decreasing = TRUE)
## # A tibble: 5 × 3
##       x y         z
##   <dbl> <chr> <dbl>
## 1     1 c     0.368
## 2     1 d     0.851
## 3     2 a     0.347
## 4     3 b     0.523
## 5    NA e     0.934
# same as
df[order(df$x, df$y), ]
## # A tibble: 5 × 3
##       x y         z
##   <dbl> <chr> <dbl>
## 1     1 c     0.368
## 2     1 d     0.851
## 3     2 a     0.347
## 4     3 b     0.523
## 5    NA e     0.934
df[order(df$x, df$y), decreasing = TRUE, ]
## # A tibble: 5 × 3
##       x y         z
##   <dbl> <chr> <dbl>
## 1     1 c     0.368
## 2     1 d     0.851
## 3     2 a     0.347
## 4     3 b     0.523
## 5    NA e     0.934

Umgedrehte Reihenfolge mit order(decreasing = TRUE), oder -rank(col)

  1. select() und relocate() zu einem Character Vector.
df |> select(x, z)
## # A tibble: 5 × 2
##       x     z
##   <dbl> <dbl>
## 1     2 0.347
## 2     3 0.523
## 3     1 0.368
## 4     1 0.851
## 5    NA 0.934
# same as
df[, c("x", "z")]
## # A tibble: 5 × 2
##       x     z
##   <dbl> <dbl>
## 1     2 0.347
## 2     3 0.523
## 3     1 0.368
## 4     1 0.851
## 5    NA 0.934

In R Base kann man auch filter() und select() kombinieren, durch subset().

df |> 
  filter(x > 1) |> 
  select(y, z)
## # A tibble: 2 × 2
##   y         z
##   <chr> <dbl>
## 1 a     0.347
## 2 b     0.523
df |> subset(x > 1, c(y, z))
## # A tibble: 2 × 2
##   y         z
##   <chr> <dbl>
## 1 a     0.347
## 2 b     0.523

Auswahl einzelner Elementen durch $ und [[

Hier zeige ich dir wie du [[ und $ benutzt, um aus Data Frames Spalten zu ziehen. Unterschiede zwischen [ und [[ werden wir in Listen kennenlernen und Unterschiede zwischen data.frames und tibbles.

Data Frames

[[ und $ können benutzt werden, um Spalten aus einem Data Frame zu ziehen. Hier könen Position oder Name bzw. der Name der Spalte stehen.

tb <- tibble(
  x = 1:4,
  y = c(10, 4, 1, 21)
)

# by position
tb[[1]]
## [1] 1 2 3 4
# by name
tb[["x"]]
## [1] 1 2 3 4
tb$x
## [1] 1 2 3 4

Wir können auch neue Spalten kreieren., so wie wir es durch mutate() schon kennen.

tb$z <- tb$x + tb$y
tb
## # A tibble: 4 × 3
##       x     y     z
##   <int> <dbl> <dbl>
## 1     1    10    11
## 2     2     4     6
## 3     3     1     4
## 4     4    21    25

Weitere Beispiele mit transform(), with() und within():

data(diamonds, package = "ggplot2")

# Most straightforward
diamonds$ppc <- diamonds$price / diamonds$carat

# Avoid repeating diamonds 
diamonds$ppc <- with(diamonds, price / carat)

# The inspiration for dplyr's mutate
diamonds <- transform(diamonds, ppc = price / carat)
diamonds <- diamonds |> transform(ppc = price / carat)

# Similar to transform(), but uses assignment rather argument matching
# (can also use = here, since = is equivalent to <- outside of a function call)
diamonds <- within(diamonds, {
  ppc <- price / carat
})
diamonds <- diamonds |> within({
  ppc <- price / carat
})

# Protect against partial matching
diamonds$ppc <- diamonds[["price"]] / diamonds[["carat"]]
diamonds$ppc <- diamonds[, "price"] / diamonds[, "carat"]

$ direkt zu benutzen ist bequem, wenn man schnelle Zusammenfassungen braucht. Dann gibt es keine Notwendigkeit summarize() zu benutzen.

max(diamonds$carat)
## [1] 5.01
levels(diamonds$cut)
## [1] "Fair"      "Good"      "Very Good" "Premium"   "Ideal"

dplyr hat auch ein Äquivalent zu [[/$ im Angebot: pull(). Es nimmt entweder einen Variablennmanen oder die Position einer Variable und gibt gerade diese Spalte aus. So können die Pipe benutzen:

diamonds |> pull(carat) |> max()
## [1] 5.01
diamonds |> pull(cut) |> levels()
## [1] "Fair"      "Good"      "Very Good" "Premium"   "Ideal"

Tibbles

In Bezug auf $ unterscheiden sich tibbles und base data.frame(). Im Gegensatz zu tibble() wird bei sata.frame() keine Fehlermeldung ausgegeben.

df <- data.frame(x1 = 1)
df$x
## [1] 1
df$z
## NULL
tb <- tibble(x1 = 1)
tb$x
## Warning: Unknown or uninitialised column: `x`.
## NULL
tb$z
## Warning: Unknown or uninitialised column: `z`.
## NULL

Listen

Der Unterschied bei Listen zu [ ist sehr wichtig zu verstehen.

l <- list(
  a = 1:3, 
  b = "a string", 
  c = pi, 
  d = list(-1, -5)
)

[ extrahiert eine Subliste.

str(l[1:2])
## List of 2
##  $ a: int [1:3] 1 2 3
##  $ b: chr "a string"
str(l[1])
## List of 1
##  $ a: int [1:3] 1 2 3
str(l[4])
## List of 1
##  $ d:List of 2
##   ..$ : num -1
##   ..$ : num -5

Bestimme die Subliste mit logical, integer oder character vector.

[[ und $ extrahieren eine einzige Komponente von einer Liste.

str(l[[1]])
##  int [1:3] 1 2 3
str(l[[4]])
## List of 2
##  $ : num -1
##  $ : num -5
str(l$a)
##  int [1:3] 1 2 3

Apply family

Das wichtigste Mitglied der Familie ist lapply(), welches sehr ähnlich zu purrr::map() ist. Du kannst jeden map() call durch lapply() ersetzen.

Es gibt kein Äquivalent zu across(), aber in R Base kommst du durch [ mit lapply() nah dran. lapply() auf einem Data Frame wendet die Funktion auf jeder Spalte an.

df <- tibble(a = 1, b = 2, c = "a", d = "b", e = 4)
# First find numeric columns
num_cols <- sapply(df, is.numeric)
num_cols
##     a     b     c     d     e 
##  TRUE  TRUE FALSE FALSE  TRUE
# Then transform each column with lapply() then replace the original values
df[, num_cols] <- lapply(df[, num_cols, drop = FALSE], \(x) x * 2)
df
## # A tibble: 1 × 5
##       a     b c     d         e
##   <dbl> <dbl> <chr> <chr> <dbl>
## 1     2     4 a     b         8

sapply() steckt den Output in einen Vektor, lapply() in eine Liste.

R bietet eine striktere Version von sapply() an: vapply(). Es nimmt ein weiteres Argument auf, dass den Typ spezifiziert.

vapply(df, is.numeric, logical(1))
##     a     b     c     d     e 
##  TRUE  TRUE FALSE FALSE  TRUE
vapply(df, is.numeric, numeric(1))
## a b c d e 
## 1 1 0 0 1

Die Ausgabe hat einen logischen Wert bzw. einen numerischen.

tapply() berechnet eine gruppierte Zusammenfassung durch eine Funktion wie mean().

diamonds |> 
  group_by(cut) |> 
  summarize(price = mean(price))
## # A tibble: 5 × 2
##   cut       price
##   <ord>     <dbl>
## 1 Fair      4359.
## 2 Good      3929.
## 3 Very Good 3982.
## 4 Premium   4584.
## 5 Ideal     3458.
tapply(diamonds$price, diamonds$cut, mean)
##      Fair      Good Very Good   Premium     Ideal 
##  4358.758  3928.864  3981.760  4584.258  3457.542

Abschließend gibt es noch apply(), welches mit Matrizen und Arrays arbeitet. Wir arbeiten aber häufiger mit Data Frames. Mehr darüber ist aber online schnell zu finden.

for loops

for Schleifen sind mächtig und wichtig und werden von Fortgeschrittenen häufig angewendet. Die Struktur sieht wie folgt aus:

for (element in vector) {
  # do something with element
}

Ein einfaches Beispiel.

k <- numeric(20)
for (i in seq(1,20)) {
  k[i] <- i*10
}
k
##  [1]  10  20  30  40  50  60  70  80  90 100 110 120 130 140 150 160 170 180 190
## [20] 200

while-Schleifen

Sie funktionieren ähnlich.

n = numeric(0)
p = 5
while(p < 10){
 n = c(n, p)
 p = p + 1
}
p
## [1] 10
n
## [1] 5 6 7 8 9

Plots

Kurz und schnell ohne Pakete.

# Left
hist(diamonds$carat)

# Right
plot(diamonds$carat, diamonds$price)